iobroker.zigbee2mqtt 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/exposes.js CHANGED
@@ -1,1350 +1,1362 @@
1
-
2
- 'use strict';
3
-
4
- const statesDefs = require('./states').states;
5
- const rgb = require('./rgb');
6
- const utils = require('./utils');
7
- const colors = require('./colors');
8
- const getNonGenDevStatesDefs = require('./nonGenericDevicesExtension').getStateDefinition;
9
-
10
- // https://www.zigbee2mqtt.io/guide/usage/exposes.html#access
11
- const z2mAccess = {
12
- /**
13
- * Bit 0: The property can be found in the published state of this device
14
- */
15
- STATE: 1,
16
- /**
17
- * Bit 1: The property can be set with a /set command
18
- */
19
- SET: 2,
20
- /**
21
- * Bit 2: The property can be retrieved with a /get command
22
- */
23
- GET: 4,
24
- /**
25
- * Bitwise inclusive OR of STATE and SET : 0b001 | 0b010
26
- */
27
- STATE_SET: 3,
28
- /**
29
- * Bitwise inclusive OR of STATE and GET : 0b001 | 0b100
30
- */
31
- STATE_GET: 5,
32
- /**
33
- * Bitwise inclusive OR of STATE and GET and SET : 0b001 | 0b100 | 0b010
34
- */
35
- ALL: 7,
36
- };
37
-
38
- function genState(expose, role, name, desc) {
39
- let state;
40
- const readable = (expose.access & z2mAccess.STATE) > 0;
41
- const writable = (expose.access & z2mAccess.SET) > 0;
42
- const stname = name || expose.property;
43
-
44
- if (typeof stname !== 'string') {
45
- return;
46
- }
47
-
48
- const stateId = stname.replace(/\*/g, '');
49
- const stateName = desc || expose.description || expose.name;
50
- const propName = expose.property;
51
-
52
- switch (expose.type) {
53
- case 'binary':
54
- state = {
55
- id: stateId,
56
- prop: propName,
57
- name: stateName,
58
- icon: undefined,
59
- role: role || 'state',
60
- write: writable,
61
- read: true,
62
- type: 'boolean',
63
- };
64
-
65
- if (readable) {
66
- state.getter = (payload) => payload[propName] === (expose.value_on || 'ON');
67
- } else {
68
- state.getter = (_payload) => undefined;
69
- }
70
-
71
- if (writable) {
72
- state.setter = (payload) =>
73
- payload ? expose.value_on || 'ON' : expose.value_off !== undefined ? expose.value_off : 'OFF';
74
- state.setattr = expose.property;
75
- }
76
-
77
- if (expose.endpoint) {
78
- state.epname = expose.endpoint;
79
- }
80
- break;
81
-
82
- case 'numeric':
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: 'number',
92
- min: expose.value_min,
93
- max: expose.value_max,
94
- unit: expose.unit,
95
- };
96
-
97
- if (expose.endpoint) {
98
- state.epname = expose.endpoint;
99
- }
100
- break;
101
-
102
- case 'enum':
103
- state = {
104
- id: stateId,
105
- prop: propName,
106
- name: stateName,
107
- icon: undefined,
108
- role: role || 'state',
109
- write: writable,
110
- read: true,
111
- states: {},
112
- };
113
-
114
- for (const val of expose.values) {
115
- // if a definition of a button (eg. Aqara presence detector FP1)
116
- if (val === '') {
117
- state.states[propName] = propName;
118
- } else {
119
- state.states[val] = val;
120
- }
121
- state.type = typeof val;
122
- }
123
- // Fix 4: wenn values leer → type bleibt undefined → Fallback auf 'string'
124
- if (!state.type) {
125
- state.type = 'string';
126
- }
127
-
128
- if (expose.endpoint) {
129
- state.epname = expose.endpoint;
130
- state.setattr = expose.name;
131
- }
132
- break;
133
-
134
- case 'text':
135
- state = {
136
- id: stateId,
137
- prop: propName,
138
- name: stateName,
139
- role: role || 'state',
140
- write: writable,
141
- read: true,
142
- type: 'string',
143
- };
144
- if (propName === 'action') {
145
- state.isEvent = true;
146
- state.getter = (payload) => {
147
- return payload[propName];
148
- };
149
- }
150
- if (expose.endpoint) {
151
- state.epname = expose.endpoint;
152
- }
153
- break;
154
-
155
- default:
156
- break;
157
- }
158
-
159
- // Try to set the state defaults
160
- if (state && state.type) {
161
- switch (state.type) {
162
- case 'boolean':
163
- state.def = false;
164
- break;
165
- case 'number':
166
- // Fix 7: negative min-Werte sollen nicht als Default dienen (z.B. Kalibrierung min=-10 → def=0)
167
- state.def = (state.min != null && state.min >= 0) ? state.min : 0;
168
- break;
169
- case 'object':
170
- state.def = {};
171
- break;
172
- case 'string':
173
- state.def = '';
174
- break;
175
- }
176
- }
177
-
178
- return state;
179
- }
180
-
181
- /**
182
- *
183
- * @param devicesMessag
184
- * @param adapter
185
- */
186
- async function createDeviceFromExposes(devicesMessag, adapter) {
187
- const states = [];
188
- let scenes = [];
189
- const config = adapter.config;
190
- const deviceID = devicesMessag.friendly_name;
191
- const ieee_address = devicesMessag.ieee_address;
192
- const definition = devicesMessag.definition;
193
- const power_source = devicesMessag.power_source;
194
- const disabled = devicesMessag.disabled === true;
195
- const description = devicesMessag.description || undefined;
196
-
197
- function pushToStates(state, access) {
198
- if (state === undefined) {
199
- return 0;
200
- }
201
- // FIX: Shallow clone verhindert Mutation von geteilten statesDefs-Objekten.
202
- // Getter/Setter sind Funktionsreferenzen und müssen nicht tief geklont werden.
203
- state = Object.assign({}, state);
204
-
205
- if (access === undefined) {access = z2mAccess.ALL;}
206
- state.readable = (access & z2mAccess.STATE) > 0;
207
- state.writable = (access & z2mAccess.SET) > 0;
208
- const stateExists = states.findIndex((x, _index, _array) => x.id === state.id);
209
-
210
- if (stateExists < 0) {
211
- state.write = state.writable;
212
- if (!state.writable) {
213
- if (state.hasOwnProperty('setter')) {
214
- delete state.setter;
215
- }
216
-
217
- if (state.hasOwnProperty('setattr')) {
218
- delete state.setattr;
219
- }
220
- }
221
-
222
- if (!state.readable) {
223
- if (state.hasOwnProperty('getter')) {
224
- //to awid some worning on unprocessed data
225
- state.getter = (_payload) => undefined;
226
- }
227
- }
228
-
229
- return states.push(state);
230
- }
231
- if (state.readable && !states[stateExists].readable) {
232
- states[stateExists].read = state.read;
233
- // as state is readable, it can't be button or event
234
- if (states[stateExists].role === 'button') {
235
- states[stateExists].role = state.role;
236
- }
237
-
238
- if (states[stateExists].hasOwnProperty('isEvent')) {
239
- delete states[stateExists].isEvent;
240
- }
241
-
242
- // we have to use the getter from "new" state
243
- if (state.hasOwnProperty('getter')) {
244
- states[stateExists].getter = state.getter;
245
- }
246
-
247
- // trying to remove the `prop` property, as main key for get and set,
248
- // as it can be different in new and old states, and leave only:
249
- // setattr for old and id for new
250
- if (state.hasOwnProperty('prop') && state.prop === state.id) {
251
- if (states[stateExists].hasOwnProperty('prop')) {
252
- if (states[stateExists].prop !== states[stateExists].id) {
253
- if (!states[stateExists].hasOwnProperty('setattr')) {
254
- states[stateExists].setattr = states[stateExists].prop;
255
- }
256
- }
257
- delete states[stateExists].prop;
258
- }
259
- } else if (state.hasOwnProperty('prop')) {
260
- states[stateExists].prop = state.prop;
261
- }
262
- states[stateExists].readable = true;
263
- }
264
-
265
- if (state.writable && !states[stateExists].writable) {
266
- states[stateExists].write = state.writable;
267
- // use new state `setter`
268
- if (state.hasOwnProperty('setter')) {
269
- states[stateExists].setter = state.setter;
270
- }
271
-
272
- // use new state `setterOpt`
273
- if (state.hasOwnProperty('setterOpt')) {
274
- states[stateExists].setterOpt = state.setterOpt;
275
- }
276
-
277
- // use new state `inOptions`
278
- if (state.hasOwnProperty('inOptions')) {
279
- states[stateExists].inOptions = state.inOptions;
280
- }
281
-
282
- // as we have new state, responsible for set, we have to use new `isOption`
283
- // or remove it
284
- if (
285
- (!state.hasOwnProperty('isOption') || state.isOption === false) && states[stateExists].hasOwnProperty('isOption')) {
286
- delete states[stateExists].isOption;
287
- } else {
288
- states[stateExists].isOption = state.isOption;
289
- }
290
-
291
- // use new `setattr` or `prop` as `setattr`
292
- if (state.hasOwnProperty('setattr')) {
293
- states[stateExists].setattr = state.setattr;
294
- } else if (state.hasOwnProperty('prop')) {
295
- states[stateExists].setattr = state.prop;
296
- }
297
-
298
- // remove `prop` equal to if, due to prop is uses as key in set and get
299
- if (states[stateExists].prop === states[stateExists].id) {
300
- delete states[stateExists].prop;
301
- }
302
-
303
- if (state.hasOwnProperty('epname')) {
304
- states[stateExists].epname = state.epname;
305
- }
306
- states[stateExists].writable = true;
307
- }
308
-
309
- return states.length;
310
-
311
- }
312
-
313
- // search for scenes in the endpoints and build them into an array
314
- for (const key of Object.keys(devicesMessag.endpoints || {})) {
315
- if (devicesMessag.endpoints[key].scenes) {
316
- scenes = scenes.concat(devicesMessag.endpoints[key].scenes);
317
- }
318
- }
319
- try {
320
- for (const expose of definition.exposes) {
321
- let state;
322
-
323
- switch (expose.type) {
324
- case 'light':
325
- for (const prop of expose.features) {
326
- switch (prop.name) {
327
- case 'state': {
328
- const stateName = expose.endpoint ? `state_${expose.endpoint}` : 'state';
329
- pushToStates(
330
- {
331
- id: stateName,
332
- name: `Switch state ${expose.endpoint ? expose.endpoint : ''}`.trim(),
333
- options: ['transition'],
334
- icon: undefined,
335
- role: 'switch',
336
- write: true,
337
- read: true,
338
- type: 'boolean',
339
- getter: (payload) => payload[stateName] === (prop.value_on || 'ON'),
340
- setter: (value) =>
341
- value
342
- ? prop.value_on || 'ON'
343
- : prop.value_off !== undefined
344
- ? prop.value_off
345
- : 'OFF',
346
- epname: expose.endpoint,
347
- //setattr: stateName,
348
- },
349
- prop.access
350
- );
351
- // features contains TOGGLE?
352
- if (prop.value_toggle) {
353
- pushToStates({
354
- id: `${stateName}_toggle`,
355
- prop: `${stateName}_toggle`,
356
- name: `Toggle state of the ${stateName}`,
357
- options: ['transition'],
358
- icon: undefined,
359
- role: 'button',
360
- write: true,
361
- read: true,
362
- type: 'boolean',
363
- def: true,
364
- setattr: stateName,
365
- setter: (value) => (value ? prop.value_toggle : undefined),
366
- });
367
- }
368
- break;
369
- }
370
- case 'brightness': {
371
- const stateName = expose.endpoint ? `brightness_${expose.endpoint}` : 'brightness';
372
- pushToStates(
373
- {
374
- id: stateName,
375
- name: `Brightness ${expose.endpoint ? expose.endpoint : ''}`.trim(),
376
- options: ['transition'],
377
- icon: undefined,
378
- role: 'level.dimmer',
379
- write: true,
380
- read: true,
381
- type: 'number',
382
- min: 0, // ignore expose.value_min
383
- max: 100, // ignore expose.value_max
384
- def: 100,
385
- unit: '%',
386
- getter: (payload) => {
387
- return utils.bulbLevelToAdapterLevel(payload[stateName]);
388
- },
389
- setter: (value) => {
390
- return utils.adapterLevelToBulbLevel(value);
391
- },
392
- },
393
- prop.access
394
- );
395
- // brightnessMoveOnOff
396
- const brmPropName =
397
- config.brightnessMoveOnOff === true
398
- ? `${stateName}_move_onoff`
399
- : `${stateName}_move`;
400
- pushToStates(
401
- {
402
- id: `${stateName}_move`,
403
- prop: brmPropName,
404
- name: 'Increases or decreases the brightness by X units per second',
405
- icon: undefined,
406
- role: 'state',
407
- write: true,
408
- read: false,
409
- type: 'number',
410
- min: -50,
411
- max: 50,
412
- def: 0,
413
- },
414
- z2mAccess.SET
415
- );
416
- // brightnessStepOnOff
417
- const brspropName =
418
- config.brightnessStepOnOff === true
419
- ? `${stateName}_step_onoff`
420
- : `${stateName}_step`;
421
- pushToStates(
422
- {
423
- id: `${stateName}_step`,
424
- prop: brspropName,
425
- name: 'Increases or decreases brightness by X steps',
426
- options: ['transition'],
427
- icon: undefined,
428
- role: 'state',
429
- write: true,
430
- read: false,
431
- type: 'number',
432
- min: -255,
433
- max: 255,
434
- def: 0,
435
- },
436
- z2mAccess.SET
437
- );
438
- break;
439
- }
440
- case 'color_temp': {
441
- const stateName = expose.endpoint ? `colortemp_${expose.endpoint}` : 'colortemp';
442
- const propName = expose.endpoint ? `color_temp_${expose.endpoint}` : 'color_temp';
443
- const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
444
- pushToStates(
445
- {
446
- id: stateName,
447
- prop: propName,
448
- name: `Color temperature ${expose.endpoint ? expose.endpoint : ''}`.trim(),
449
- options: ['transition'],
450
- icon: undefined,
451
- role: 'level.color.temperature',
452
- write: true,
453
- read: true,
454
- type: 'number',
455
- min:
456
- config.useKelvin === true
457
- ? utils.miredKelvinConversion(prop.value_max)
458
- : prop.value_min,
459
- max:
460
- config.useKelvin === true
461
- ? utils.miredKelvinConversion(prop.value_min)
462
- : prop.value_max,
463
- def:
464
- config.useKelvin === true
465
- ? utils.miredKelvinConversion(prop.value_min)
466
- : prop.value_max,
467
- unit: config.useKelvin === true ? 'K' : 'mired',
468
- setter: (value) => {
469
- return utils.toMired(value);
470
- },
471
- getter: (payload) => {
472
- if (payload[colorMode] !== 'color_temp') {
473
- return undefined;
474
- }
475
- // Fix 10: null-Check vor miredKelvinConversion
476
- const val = payload[propName];
477
- if (val == null) {return undefined;}
478
- if (config.useKelvin === true) {
479
- return utils.miredKelvinConversion(val);
480
- }
481
- return val;
482
- },
483
- },
484
- prop.access
485
- );
486
- // Colortemp move – nur schreibbar (SET), nicht readable
487
- pushToStates(
488
- {
489
- id: `${stateName}_move`,
490
- prop: `${propName}_move`,
491
- name: 'Colortemp change',
492
- icon: undefined,
493
- role: 'state',
494
- write: true,
495
- read: false,
496
- type: 'number',
497
- min: -50,
498
- max: 50,
499
- def: 0,
500
- },
501
- z2mAccess.SET // FIX: war prop.access → konnte fälschlicherweise readable werden
502
- );
503
- break;
504
- }
505
- case 'color_temp_startup': {
506
- //const stateName = expose.endpoint
507
- // ? `colortempstartup_${expose.endpoint}`
508
- // : 'colortempstartup';
509
- const propName = expose.endpoint
510
- ? `color_temp_startup_${expose.endpoint}`
511
- : 'color_temp_startup';
512
- //const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
513
- pushToStates(
514
- {
515
- id: propName,
516
- prop: propName,
517
- name: `${prop.description} ${expose.endpoint ? `(${expose.endpoint})` : ''}`.trim(),
518
- //options: ['transition'],
519
- icon: undefined,
520
- role: 'level', // Changed role to level to avoid double level.temperature in one device
521
- write: true,
522
- read: true,
523
- type: 'number',
524
- min: 0,
525
- max: 65535,
526
- def: undefined,
527
- unit: config.useKelvin === true ? 'K' : 'mired',
528
- setter: (value) => {
529
- return utils.toMired(value);
530
- },
531
- getter: (payload) => {
532
- const val = payload[propName];
533
- if (val == null) {
534
- return undefined;
535
- }
536
- if (config.useKelvin === true) {
537
- return utils.miredKelvinConversion(val);
538
- }
539
- return val;
540
- },
541
- },
542
- prop.access
543
- );
544
- break;
545
- }
546
- case 'color_xy': {
547
- const stateName = expose.endpoint ? `color_${expose.endpoint}` : 'color';
548
- const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
549
- pushToStates(
550
- {
551
- id: stateName,
552
- name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
553
- options: ['transition'],
554
- icon: undefined,
555
- role: 'level.color.rgb',
556
- write: true,
557
- read: true,
558
- type: 'string',
559
- def: '#ff00ff',
560
- setter: (value) => {
561
- // Fix 1: redundante [0,0]-Initialisierung entfernt
562
- const rgbcolor = colors.ParseColor(value);
563
- const xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
564
- return {
565
- x: xy[0],
566
- y: xy[1],
567
- };
568
- },
569
- getter: (payload) => {
570
- if (payload[colorMode] !== 'xy' && !config.colorTempSyncColor) {
571
- return undefined;
572
- }
573
- // Fix 2: x=0/y=0 sind gültige CIE-Koordinaten → != null
574
- if (
575
- payload[stateName] &&
576
- payload[stateName].x != null &&
577
- payload[stateName].y != null
578
- ) {
579
- const colorval = rgb.cie_to_rgb(
580
- payload[stateName].x,
581
- payload[stateName].y
582
- );
583
- return (
584
- `#${utils.decimalToHex(colorval[0])
585
- }${utils.decimalToHex(colorval[1])
586
- }${utils.decimalToHex(colorval[2])}`
587
- );
588
- }
589
- return undefined;
590
- },
591
- epname: expose.endpoint,
592
- },
593
- prop.access
594
- );
595
- break;
596
- }
597
- case 'color_hs': {
598
- const stateName = expose.endpoint ? `color_${expose.endpoint}` : 'color';
599
- const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
600
- pushToStates(
601
- {
602
- id: stateName,
603
- name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
604
- options: ['transition'],
605
- icon: undefined,
606
- role: 'level.color.rgb',
607
- write: true,
608
- read: true,
609
- type: 'string',
610
- def: '#ff00ff',
611
- setter: (value) => {
612
- const _rgb = colors.ParseColor(value);
613
- const hsv = rgb.rgbToHSV(_rgb.r, _rgb.g, _rgb.b, true);
614
- return {
615
- h: Math.min(Math.max(hsv.h, 1), 359),
616
- s: hsv.s,
617
- //b: Math.round(hsv.v * 2.55),
618
- };
619
- },
620
- getter: (payload) => {
621
- if (
622
- !['hs', 'xy'].includes(payload[colorMode]) &&
623
- !config.colorTempSyncColor
624
- ) {
625
- return undefined;
626
- }
627
- // Fix 3: h=0 (Rot) ist falsy → != null verwenden
628
- if (
629
- payload[stateName] &&
630
- payload[stateName].h != null &&
631
- payload[stateName].s != null &&
632
- payload[stateName].b != null
633
- ) {
634
- return rgb.hsvToRGBString(
635
- payload[stateName].h,
636
- payload[stateName].s,
637
- Math.round(payload[stateName].b / 2.55)
638
- );
639
- }
640
- if (
641
- payload[stateName] &&
642
- payload[stateName].x != null &&
643
- payload[stateName].y != null
644
- ) {
645
- const colorval = rgb.cie_to_rgb(
646
- payload[stateName].x,
647
- payload[stateName].y
648
- );
649
- return (
650
- `#${utils.decimalToHex(colorval[0])
651
- }${utils.decimalToHex(colorval[1])
652
- }${utils.decimalToHex(colorval[2])}`
653
- );
654
- }
655
- return undefined;
656
- },
657
- },
658
- prop.access
659
- );
660
- break;
661
- }
662
- default:
663
- pushToStates(genState(prop), prop.access);
664
- break;
665
- }
666
- }
667
- pushToStates(statesDefs.transition, z2mAccess.SET);
668
- break;
669
-
670
- case 'switch':
671
- for (const prop of expose.features) {
672
- switch (prop.name) {
673
- case 'state':
674
- pushToStates(genState(prop, 'switch'), prop.access);
675
- // features contains TOGGLE?
676
- if (prop.value_toggle) {
677
- pushToStates({
678
- id: `${prop.property}_toggle`,
679
- prop: `${prop.property}_toggle`,
680
- name: `Toggle state of the ${prop.property}`,
681
- icon: undefined,
682
- role: 'button',
683
- write: true,
684
- read: true,
685
- type: 'boolean',
686
- def: true,
687
- setattr: prop.property,
688
- setter: (value) => (value ? prop.value_toggle : undefined),
689
- });
690
- }
691
- break;
692
- default:
693
- pushToStates(genState(prop), prop.access);
694
- break;
695
- }
696
- }
697
- break;
698
-
699
- case 'numeric':
700
- if (expose.endpoint) {
701
- state = genState(expose);
702
- } else {
703
- switch (expose.name) {
704
- case 'linkquality':
705
- state = statesDefs.link_quality;
706
- break;
707
-
708
- case 'battery':
709
- state = statesDefs.battery;
710
- break;
711
-
712
- case 'temperature':
713
- state = statesDefs.temperature;
714
- break;
715
-
716
- case 'device_temperature':
717
- state = statesDefs.device_temperature;
718
- break;
719
-
720
- case 'humidity':
721
- state = statesDefs.humidity;
722
- break;
723
-
724
- case 'pressure':
725
- state = statesDefs.pressure;
726
- break;
727
-
728
- case 'illuminance':
729
- // Z2M sendet 'illuminance' = Rohwert (kein lux)
730
- // 'illuminance_lux' = Wert in Lux → eigener case unten
731
- state = statesDefs.illuminance_raw;
732
- break;
733
-
734
- case 'illuminance_lux':
735
- state = statesDefs.illuminance;
736
- break;
737
-
738
- case 'power':
739
- state = statesDefs.load_power;
740
- break;
741
-
742
- case 'current':
743
- state = statesDefs.load_current;
744
- break;
745
-
746
- case 'voltage':
747
- // FIX: Klone vor Mutation, damit statesDefs.voltage nicht dauerhaft verändert wird
748
- state = Object.assign({}, power_source === 'Battery' ? statesDefs.battery_voltage : statesDefs.voltage);
749
- if (expose.unit === 'mV') {
750
- // Fix 7: expose.property statt hardcoded 'voltage'
751
- const voltProp = expose.property;
752
- state.getter = (payload) => payload[voltProp] != null ? payload[voltProp] / 1000 : undefined;
753
- }
754
- break;
755
-
756
- case 'energy':
757
- state = statesDefs.energy;
758
- break;
759
-
760
- default:
761
- state = genState(expose);
762
- break;
763
- }
764
- }
765
- if (state) {pushToStates(state, expose.access);}
766
- break;
767
-
768
- case 'enum':
769
- switch (expose.name) {
770
- case 'action': {
771
- // generate an 'action' state
772
- state = genState(expose);
773
- state.isEvent = true;
774
- state.getter = (payload) => payload.action;
775
- pushToStates(state, expose.access);
776
- state = null;
777
-
778
- if (!Array.isArray(expose.values)) {
779
- break;
780
- }
781
-
782
- // Support for DIYRuZ Device
783
- const wildcardValues = expose.values.filter((x) => x.startsWith('*'));
784
- if (wildcardValues && wildcardValues.length > 0) {
785
- for (const endpointName of [
786
- ...new Set(definition.exposes.filter((x) => x.endpoint).map((x) => x.endpoint)),
787
- ]) {
788
- for (const value of wildcardValues) {
789
- const actionName = value.replace('*', endpointName);
790
- pushToStates(
791
- {
792
- id: actionName,
793
- prop: 'action',
794
- name: `Triggered action ${value.replace('*_', endpointName)}`,
795
- icon: undefined,
796
- role: 'button',
797
- write: false,
798
- read: true,
799
- type: 'boolean',
800
- def: false,
801
- isEvent: true,
802
- getter: (payload) => (payload.action === actionName ? true : undefined),
803
- },
804
- expose.access
805
- );
806
- }
807
- }
808
- break;
809
- }
810
-
811
- for (const actionName of expose.values) {
812
- // is release -> hold state? - skip
813
- if (
814
- config.simpleHoldReleaseState === true &&
815
- actionName.endsWith('release') &&
816
- expose.values.find((x) => x === actionName.replace('release', 'hold'))
817
- ) {
818
- continue;
819
- }
820
-
821
- // is stop - move state? - skip
822
- if (
823
- config.simpleMoveStopState === true &&
824
- actionName.endsWith('stop') &&
825
- expose.values.find((x) => x.includes(actionName.replace('stop', 'move')))
826
- ) {
827
- continue;
828
- }
829
-
830
- // is release -> press state? - skip
831
- if (
832
- config.simplePressReleaseState === true &&
833
- actionName.endsWith('release') &&
834
- expose.values.find((x) => x === actionName.replace('release', 'press'))
835
- ) {
836
- continue;
837
- }
838
-
839
- // is hold -> release state ?
840
- if (
841
- config.simpleHoldReleaseState === true &&
842
- actionName.endsWith('hold') &&
843
- expose.values.find((x) => x === actionName.replace('hold', 'release'))
844
- ) {
845
- pushToStates(
846
- {
847
- id: actionName.replace(/\*/g, ''),
848
- prop: 'action',
849
- name: actionName,
850
- icon: undefined,
851
- role: 'button',
852
- write: false,
853
- read: true,
854
- def: false,
855
- type: 'boolean',
856
- getter: (payload) => {
857
- if (payload.action === actionName) {
858
- return true;
859
- }
860
- if (payload.action === actionName.replace('hold', 'release')) {
861
- return false;
862
- }
863
- if (payload.action === `${actionName}_release`) {
864
- return false;
865
- }
866
- return undefined;
867
- },
868
- },
869
- expose.access
870
- );
871
- }
872
- // is move -> stop state ?
873
- else if (
874
- config.simpleMoveStopState === true &&
875
- actionName.includes('move') &&
876
- expose.values.find((x) => x === `${actionName.split('_')[0]}_stop`)
877
- ) {
878
- pushToStates(
879
- {
880
- id: actionName.replace(/\*/g, ''),
881
- prop: 'action',
882
- name: actionName,
883
- icon: undefined,
884
- role: 'button',
885
- write: false,
886
- read: true,
887
- def: false,
888
- type: 'boolean',
889
- getter: (payload) => {
890
- if (payload.action === actionName) {
891
- return true;
892
- }
893
- if (payload.action === `${actionName.split('_')[0]}_stop`) {
894
- return false;
895
- }
896
- return undefined;
897
- },
898
- },
899
- expose.access
900
- );
901
- }
902
- // is press -> release state ?
903
- else if (
904
- config.simplePressReleaseState === true &&
905
- actionName.endsWith('press') &&
906
- expose.values.find((x) => x === actionName.replace('press', 'release'))
907
- ) {
908
- pushToStates(
909
- {
910
- id: actionName.replace(/\*/g, ''),
911
- prop: 'action',
912
- name: actionName,
913
- icon: undefined,
914
- role: 'button',
915
- write: false,
916
- read: true,
917
- def: false,
918
- type: 'boolean',
919
- getter: (payload) => {
920
- if (payload.action === actionName) {
921
- return true;
922
- }
923
- if (payload.action === actionName.replace('press', 'release')) {
924
- return false;
925
- }
926
- return undefined;
927
- },
928
- },
929
- expose.access
930
- );
931
- } else if (actionName === 'color_temperature_move') {
932
- pushToStates(
933
- {
934
- id: 'color_temperature_move',
935
- prop: 'action',
936
- name: 'Color temperature move value',
937
- icon: undefined,
938
- role: 'level.color.temperature',
939
- write: false,
940
- read: true,
941
- type: 'number',
942
- def: config.useKelvin === true ? utils.miredKelvinConversion(150) : 500,
943
- min: config.useKelvin === true ? utils.miredKelvinConversion(500) : 150,
944
- max: config.useKelvin === true ? utils.miredKelvinConversion(150) : 500,
945
- unit: config.useKelvin === true ? 'K' : 'mired',
946
- isEvent: true,
947
- getter: (payload) => {
948
- if (payload.action !== 'color_temperature_move') {
949
- return undefined;
950
- }
951
- // Fix 4: != null statt truthy (0 ist ein gültiger Mired-Wert)
952
- if (payload.action_color_temperature != null) {
953
- if (config.useKelvin === true) {
954
- return utils.miredKelvinConversion(
955
- payload.action_color_temperature
956
- );
957
- }
958
- return payload.action_color_temperature;
959
- }
960
- return undefined;
961
- },
962
- },
963
- expose.access
964
- );
965
- } else if (actionName === 'color_move') {
966
- pushToStates(
967
- {
968
- id: 'color_move',
969
- prop: 'action',
970
- name: 'Color move value',
971
- icon: undefined,
972
- role: 'level.color.rgb',
973
- write: false,
974
- read: true,
975
- type: 'string',
976
- def: '#ffffff',
977
- isEvent: true,
978
- getter: (payload) => {
979
- if (payload.action !== 'color_move') {
980
- return undefined;
981
- }
982
-
983
- if (
984
- payload.action_color &&
985
- payload.action_color.hasOwnProperty('x') &&
986
- payload.action_color.hasOwnProperty('y')
987
- ) {
988
- const colorval = rgb.cie_to_rgb(
989
- payload.action_color.x,
990
- payload.action_color.y
991
- );
992
- return (
993
- `#${
994
- utils.decimalToHex(colorval[0])
995
- }${utils.decimalToHex(colorval[1])
996
- }${utils.decimalToHex(colorval[2])}`
997
- );
998
- }
999
- return undefined;
1000
-
1001
- },
1002
- },
1003
- expose.access
1004
- );
1005
- } else if (actionName === 'brightness_move_to_level') {
1006
- pushToStates(
1007
- {
1008
- id: 'brightness_move_to_level',
1009
- prop: 'action', // Fix 1: fehlte → statesController fand State nie
1010
- name: 'Brightness move to level',
1011
- icon: undefined,
1012
- role: 'level.dimmer',
1013
- write: false,
1014
- read: true,
1015
- type: 'number',
1016
- min: 0,
1017
- max: 100,
1018
- def: 100,
1019
- unit: '%',
1020
- isEvent: true,
1021
- getter: (payload) => {
1022
- if (payload.action !== 'brightness_move_to_level') {
1023
- return undefined;
1024
- }
1025
- // Fix 3: != null statt truthy (action_level=0 ist gültig)
1026
- if (payload.action_level != null) {
1027
- return utils.bulbLevelToAdapterLevel(payload.action_level);
1028
- }
1029
- return undefined;
1030
- },
1031
- },
1032
- expose.access
1033
- );
1034
- } else if (actionName === 'move_to_saturation') {
1035
- pushToStates(
1036
- {
1037
- id: 'move_to_saturation',
1038
- prop: 'action', // Fix 2: fehlte → statesController fand State nie
1039
- name: 'Move to level saturation',
1040
- icon: undefined,
1041
- role: 'level.color.saturation',
1042
- write: false,
1043
- read: true,
1044
- type: 'number',
1045
- // min: 0,
1046
- // max: 100,
1047
- def: 0,
1048
- isEvent: true,
1049
- getter: (payload) => {
1050
- if (payload.action !== 'move_to_saturation') {
1051
- return undefined;
1052
- }
1053
- // FIX: action_saturation prüfen, nicht action_level
1054
- if (payload.action_saturation != null) {
1055
- return payload.action_saturation;
1056
- }
1057
- return undefined;
1058
- },
1059
- },
1060
- expose.access
1061
- );
1062
- } else if (actionName === 'enhanced_move_to_hue_and_saturation') {
1063
- pushToStates(
1064
- {
1065
- id: 'enhanced_move_to_hue_and_saturation',
1066
- prop: 'action',
1067
- name: 'Enhanced move to hue and saturation value',
1068
- icon: undefined,
1069
- role: 'level.color.hue',
1070
- write: false,
1071
- read: true,
1072
- type: 'number',
1073
- min: 0,
1074
- max: 65536,
1075
- def: 0,
1076
- isEvent: true,
1077
- getter: (payload) => {
1078
- if (payload.action !== 'enhanced_move_to_hue_and_saturation') {
1079
- return undefined;
1080
- }
1081
- // Fix 5: != null statt truthy (0 ist gültiger Hue-Wert)
1082
- if (payload.action_enhanced_hue != null) {
1083
- return payload.action_enhanced_hue;
1084
- }
1085
- return undefined;
1086
- },
1087
- },
1088
- expose.access
1089
- );
1090
- }
1091
-
1092
- else {
1093
- pushToStates(
1094
- {
1095
- id: actionName.replace(/\*/g, ''),
1096
- prop: 'action',
1097
- name: actionName,
1098
- icon: undefined,
1099
- role: 'button',
1100
- write: false,
1101
- read: true,
1102
- type: 'boolean',
1103
- def: false,
1104
- isEvent: true,
1105
- getter: (payload) => (payload.action === actionName ? true : undefined),
1106
- },
1107
- expose.access
1108
- );
1109
- }
1110
- }
1111
- // Can the device simulated_brightness?
1112
- if (
1113
- definition.options &&
1114
- definition.options.find((x) => x.property === 'simulated_brightness')
1115
- ) {
1116
- pushToStates(statesDefs.simulated_brightness, z2mAccess.STATE);
1117
- }
1118
- state = null;
1119
- break;
1120
- }
1121
- default:
1122
- state = genState(expose);
1123
- break;
1124
- }
1125
- if (state) {pushToStates(state, expose.access);}
1126
- break;
1127
-
1128
- case 'binary':
1129
- if (expose.endpoint) {
1130
- state = genState(expose);
1131
- } else {
1132
- switch (expose.name) {
1133
- case 'contact':
1134
- state = statesDefs.contact;
1135
- pushToStates(statesDefs.opened, expose.access);
1136
- break;
1137
-
1138
- case 'battery_low':
1139
- state = statesDefs.batt_low_t_f;
1140
- break;
1141
-
1142
- case 'tamper':
1143
- state = statesDefs.tamper;
1144
- break;
1145
-
1146
- case 'water_leak':
1147
- state = statesDefs.water_leak;
1148
- break;
1149
-
1150
- case 'lock':
1151
- state = statesDefs.child_lock;
1152
- break;
1153
-
1154
- case 'occupancy':
1155
- state = statesDefs.occupancy;
1156
- break;
1157
-
1158
- default:
1159
- state = genState(expose);
1160
- break;
1161
- }
1162
- }
1163
- if (state) {pushToStates(state, expose.access);}
1164
- break;
1165
-
1166
- case 'list':
1167
- // Z2M 'list' type → Array-Wert als JSON-String speichern
1168
- pushToStates({
1169
- id: expose.property,
1170
- prop: expose.property,
1171
- name: expose.description || expose.name || expose.property,
1172
- icon: undefined,
1173
- role: 'json',
1174
- write: (expose.access & z2mAccess.SET) > 0,
1175
- read: true,
1176
- type: 'string',
1177
- def: '[]',
1178
- getter: (payload) => {
1179
- const val = payload[expose.property];
1180
- if (val == null) {return undefined;}
1181
- return typeof val === 'string' ? val : JSON.stringify(val);
1182
- },
1183
- }, expose.access);
1184
- break;
1185
-
1186
- case 'text':
1187
- state = genState(expose);
1188
- pushToStates(state, expose.access);
1189
- break;
1190
-
1191
- case 'lock':
1192
- case 'fan':
1193
- case 'cover':
1194
- for (const prop of expose.features) {
1195
- switch (prop.name) {
1196
- case 'state':
1197
- pushToStates(genState(prop, 'switch'), prop.access);
1198
- // features contains TOGGLE?
1199
- if (prop.value_toggle) {
1200
- pushToStates({
1201
- id: `${prop.property}_toggle`,
1202
- prop: `${prop.property}_toggle`,
1203
- name: `Toggle state of the ${prop.property}`,
1204
- icon: undefined,
1205
- role: 'button',
1206
- write: true,
1207
- read: true,
1208
- def: true,
1209
- type: 'boolean',
1210
- setattr: prop.property,
1211
- setter: (value) => (value ? prop.value_toggle : undefined),
1212
- });
1213
- }
1214
- break;
1215
- default:
1216
- pushToStates(genState(prop), prop.access);
1217
- break;
1218
- }
1219
- }
1220
- break;
1221
-
1222
- case 'climate':
1223
- for (const prop of expose.features) {
1224
- switch (prop.name) {
1225
- case 'away_mode':
1226
- pushToStates(statesDefs.climate_away_mode, prop.access);
1227
- break;
1228
- case 'system_mode':
1229
- pushToStates(statesDefs.climate_system_mode, prop.access);
1230
- break;
1231
- case 'running_mode':
1232
- case 'running_state': // Z2M nutzt beide Bezeichnungen je nach Version
1233
- pushToStates(statesDefs.climate_running_mode, prop.access);
1234
- break;
1235
- case 'local_temperature':
1236
- pushToStates(statesDefs.local_temperature, prop.access);
1237
- break;
1238
- case 'local_temperature_calibration':
1239
- pushToStates(statesDefs.local_temperature_calibration, prop.access);
1240
- break;
1241
- default:
1242
- {
1243
- if (prop.name.includes('heating_setpoint')) {
1244
- pushToStates(genState(prop, 'level.temperature'), prop.access);
1245
- } else {
1246
- pushToStates(genState(prop), prop.access);
1247
- }
1248
- }
1249
- break;
1250
- }
1251
- }
1252
- break;
1253
-
1254
- case 'composite': {
1255
- for (const propRaw of expose.features) {
1256
- // FIX: Clone um Mutation des originalen Z2M-Expose-Objekts zu vermeiden
1257
- // (prop.type = 'text' würde sonst beim nächsten bridge/devices-Event noch 'text' sein)
1258
- const prop = Object.assign({}, propRaw);
1259
- prop.type = 'text'; // to avoid problems with numbers, booleans, etc.
1260
-
1261
- const state = genState(prop);
1262
- // Workaround for FP1 new state (region_upsert)
1263
- if (!state) {
1264
- break;
1265
- }
1266
-
1267
- state.prop = expose.property;
1268
- state.inOptions = true;
1269
- state.isOption = true;
1270
-
1271
- if (expose.access & z2mAccess.STATE) {
1272
- state.getter = (payload) => {
1273
- if (
1274
- payload.hasOwnProperty(expose.property) &&
1275
- payload[expose.property] !== null &&
1276
- payload[expose.property].hasOwnProperty(prop.property)
1277
- ) {
1278
- return !isNaN(payload[expose.property][prop.property])
1279
- ? payload[expose.property][prop.property]
1280
- : undefined;
1281
- }
1282
- return undefined;
1283
-
1284
- };
1285
- } else {
1286
- state.getter = (payload) => {
1287
- // Fix: null-Check vor Zugriff auf property
1288
- if (!payload[expose.property]) {
1289
- return undefined;
1290
- }
1291
- return payload[expose.property][prop.property];
1292
- };
1293
- }
1294
-
1295
- pushToStates(state, z2mAccess.STATE);
1296
- }
1297
-
1298
- break;
1299
- }
1300
- default:
1301
- adapter.log.debug(`Unhandled expose type ${expose.type} for device ${deviceID}`);
1302
- }
1303
- }
1304
- } catch (err) {
1305
- adapter.log.error(`ERROR in expose for device ${deviceID} : ${err}`);
1306
- }
1307
-
1308
- // Fix 8: definition.model kann undefined sein → Fallback auf ''
1309
- for (const state of getNonGenDevStatesDefs(definition.model || '')) {
1310
- pushToStates(state, state.write ? z2mAccess.SET : z2mAccess.STATE);
1311
- }
1312
-
1313
- // Add default states
1314
- pushToStates(statesDefs.available, z2mAccess.STATE);
1315
- pushToStates(statesDefs.last_seen, z2mAccess.STATE);
1316
- pushToStates(statesDefs.send_payload, z2mAccess.SET);
1317
-
1318
- // Create buttons for scenes
1319
- for (const scene of scenes) {
1320
- // Fix 9: scene.name kann null/undefined sein
1321
- pushToStates({
1322
- id: `scene_${scene.id}`,
1323
- prop: `scene_recall`,
1324
- name: scene.name || `Scene ${scene.id}`,
1325
- icon: undefined,
1326
- role: 'button',
1327
- write: true,
1328
- read: true,
1329
- def: true,
1330
- type: 'boolean',
1331
- setter: (value) => (value ? scene.id : undefined),
1332
- });
1333
- }
1334
-
1335
- const newDevice = {
1336
- id: deviceID,
1337
- ieee_address: ieee_address,
1338
- power_source: power_source,
1339
- disabled: disabled,
1340
- description: description,
1341
- optionsValues: {},
1342
- states: states,
1343
- };
1344
-
1345
- return newDevice;
1346
- }
1347
-
1348
- module.exports = {
1349
- createDeviceFromExposes: createDeviceFromExposes,
1350
- };
1
+
2
+ 'use strict';
3
+
4
+ const statesDefs = require('./states').states;
5
+ const rgb = require('./rgb');
6
+ const utils = require('./utils');
7
+ const colors = require('./colors');
8
+ const getNonGenDevStatesDefs = require('./nonGenericDevicesExtension').getStateDefinition;
9
+
10
+ // https://www.zigbee2mqtt.io/guide/usage/exposes.html#access
11
+ const z2mAccess = {
12
+ /**
13
+ * Bit 0: The property can be found in the published state of this device
14
+ */
15
+ STATE: 1,
16
+ /**
17
+ * Bit 1: The property can be set with a /set command
18
+ */
19
+ SET: 2,
20
+ /**
21
+ * Bit 2: The property can be retrieved with a /get command
22
+ */
23
+ GET: 4,
24
+ /**
25
+ * Bitwise inclusive OR of STATE and SET : 0b001 | 0b010
26
+ */
27
+ STATE_SET: 3,
28
+ /**
29
+ * Bitwise inclusive OR of STATE and GET : 0b001 | 0b100
30
+ */
31
+ STATE_GET: 5,
32
+ /**
33
+ * Bitwise inclusive OR of STATE and GET and SET : 0b001 | 0b100 | 0b010
34
+ */
35
+ ALL: 7,
36
+ };
37
+
38
+ function genState(expose, role, name, desc) {
39
+ let state;
40
+ const readable = (expose.access & z2mAccess.STATE) > 0;
41
+ const writable = (expose.access & z2mAccess.SET) > 0;
42
+ const stname = name || expose.property;
43
+
44
+ if (typeof stname !== 'string') {
45
+ return;
46
+ }
47
+
48
+ const stateId = stname.replace(/\*/g, '');
49
+ const stateName = desc || expose.description || expose.name;
50
+ const propName = expose.property;
51
+
52
+ switch (expose.type) {
53
+ case 'binary':
54
+ state = {
55
+ id: stateId,
56
+ prop: propName,
57
+ name: stateName,
58
+ icon: undefined,
59
+ role: role || 'state',
60
+ write: writable,
61
+ read: true,
62
+ type: 'boolean',
63
+ };
64
+
65
+ if (readable) {
66
+ state.getter = (payload) => payload[propName] === (expose.value_on || 'ON');
67
+ } else {
68
+ state.getter = (_payload) => undefined;
69
+ }
70
+
71
+ if (writable) {
72
+ state.setter = (payload) =>
73
+ payload ? expose.value_on || 'ON' : expose.value_off !== undefined ? expose.value_off : 'OFF';
74
+ state.setattr = expose.property;
75
+ }
76
+
77
+ if (expose.endpoint) {
78
+ state.epname = expose.endpoint;
79
+ }
80
+ break;
81
+
82
+ case 'numeric':
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: 'number',
92
+ min: expose.value_min,
93
+ max: expose.value_max,
94
+ unit: expose.unit,
95
+ };
96
+
97
+ if (expose.endpoint) {
98
+ state.epname = expose.endpoint;
99
+ }
100
+ break;
101
+
102
+ case 'enum':
103
+ state = {
104
+ id: stateId,
105
+ prop: propName,
106
+ name: stateName,
107
+ icon: undefined,
108
+ role: role || 'state',
109
+ write: writable,
110
+ read: true,
111
+ states: {},
112
+ };
113
+
114
+ for (const val of expose.values) {
115
+ // if a definition of a button (eg. Aqara presence detector FP1)
116
+ if (val === '') {
117
+ state.states[propName] = propName;
118
+ } else {
119
+ state.states[val] = val;
120
+ }
121
+ state.type = typeof val;
122
+ }
123
+ // Fix 4: wenn values leer → type bleibt undefined → Fallback auf 'string'
124
+ if (!state.type) {
125
+ state.type = 'string';
126
+ }
127
+
128
+ if (expose.endpoint) {
129
+ state.epname = expose.endpoint;
130
+ state.setattr = expose.name;
131
+ }
132
+ break;
133
+
134
+ case 'text':
135
+ state = {
136
+ id: stateId,
137
+ prop: propName,
138
+ name: stateName,
139
+ role: role || 'state',
140
+ write: writable,
141
+ read: true,
142
+ type: 'string',
143
+ };
144
+ if (propName === 'action') {
145
+ state.isEvent = true;
146
+ state.getter = (payload) => {
147
+ return payload[propName];
148
+ };
149
+ }
150
+ if (expose.endpoint) {
151
+ state.epname = expose.endpoint;
152
+ }
153
+ break;
154
+
155
+ default:
156
+ break;
157
+ }
158
+
159
+ // Try to set the state defaults
160
+ if (state && state.type) {
161
+ switch (state.type) {
162
+ case 'boolean':
163
+ state.def = false;
164
+ break;
165
+ case 'number':
166
+ // Fix 7: negative min-Werte sollen nicht als Default dienen (z.B. Kalibrierung min=-10 → def=0)
167
+ state.def = (state.min != null && state.min >= 0) ? state.min : 0;
168
+ break;
169
+ case 'object':
170
+ state.def = {};
171
+ break;
172
+ case 'string':
173
+ state.def = '';
174
+ break;
175
+ }
176
+ }
177
+
178
+ return state;
179
+ }
180
+
181
+ /**
182
+ *
183
+ * @param devicesMessag
184
+ * @param adapter
185
+ */
186
+ async function createDeviceFromExposes(devicesMessag, adapter) {
187
+ const states = [];
188
+ let scenes = [];
189
+ const config = adapter.config;
190
+ const deviceID = devicesMessag.friendly_name;
191
+ const ieee_address = devicesMessag.ieee_address;
192
+ const definition = devicesMessag.definition;
193
+ const power_source = devicesMessag.power_source;
194
+ const disabled = devicesMessag.disabled === true;
195
+ const description = devicesMessag.description || undefined;
196
+
197
+ function pushToStates(state, access) {
198
+ if (state === undefined) {
199
+ return 0;
200
+ }
201
+ // FIX: Shallow clone verhindert Mutation von geteilten statesDefs-Objekten.
202
+ // Getter/Setter sind Funktionsreferenzen und müssen nicht tief geklont werden.
203
+ state = Object.assign({}, state);
204
+
205
+ if (access === undefined) {access = z2mAccess.ALL;}
206
+ state.readable = (access & z2mAccess.STATE) > 0;
207
+ state.writable = (access & z2mAccess.SET) > 0;
208
+ const stateExists = states.findIndex((x, _index, _array) => x.id === state.id);
209
+
210
+ if (stateExists < 0) {
211
+ state.write = state.writable;
212
+ if (!state.writable) {
213
+ if (state.hasOwnProperty('setter')) {
214
+ delete state.setter;
215
+ }
216
+
217
+ if (state.hasOwnProperty('setattr')) {
218
+ delete state.setattr;
219
+ }
220
+ }
221
+
222
+ if (!state.readable) {
223
+ if (state.hasOwnProperty('getter')) {
224
+ //to awid some worning on unprocessed data
225
+ state.getter = (_payload) => undefined;
226
+ }
227
+ }
228
+
229
+ return states.push(state);
230
+ }
231
+ if (state.readable && !states[stateExists].readable) {
232
+ states[stateExists].read = state.read;
233
+ // as state is readable, it can't be button or event
234
+ if (states[stateExists].role === 'button') {
235
+ states[stateExists].role = state.role;
236
+ }
237
+
238
+ if (states[stateExists].hasOwnProperty('isEvent')) {
239
+ delete states[stateExists].isEvent;
240
+ }
241
+
242
+ // we have to use the getter from "new" state
243
+ if (state.hasOwnProperty('getter')) {
244
+ states[stateExists].getter = state.getter;
245
+ }
246
+
247
+ // trying to remove the `prop` property, as main key for get and set,
248
+ // as it can be different in new and old states, and leave only:
249
+ // setattr for old and id for new
250
+ if (state.hasOwnProperty('prop') && state.prop === state.id) {
251
+ if (states[stateExists].hasOwnProperty('prop')) {
252
+ if (states[stateExists].prop !== states[stateExists].id) {
253
+ if (!states[stateExists].hasOwnProperty('setattr')) {
254
+ states[stateExists].setattr = states[stateExists].prop;
255
+ }
256
+ }
257
+ delete states[stateExists].prop;
258
+ }
259
+ } else if (state.hasOwnProperty('prop')) {
260
+ states[stateExists].prop = state.prop;
261
+ }
262
+ states[stateExists].readable = true;
263
+ }
264
+
265
+ if (state.writable && !states[stateExists].writable) {
266
+ states[stateExists].write = state.writable;
267
+ // use new state `setter`
268
+ if (state.hasOwnProperty('setter')) {
269
+ states[stateExists].setter = state.setter;
270
+ }
271
+
272
+ // use new state `setterOpt`
273
+ if (state.hasOwnProperty('setterOpt')) {
274
+ states[stateExists].setterOpt = state.setterOpt;
275
+ }
276
+
277
+ // use new state `inOptions`
278
+ if (state.hasOwnProperty('inOptions')) {
279
+ states[stateExists].inOptions = state.inOptions;
280
+ }
281
+
282
+ // as we have new state, responsible for set, we have to use new `isOption`
283
+ // or remove it
284
+ if (
285
+ (!state.hasOwnProperty('isOption') || state.isOption === false) && states[stateExists].hasOwnProperty('isOption')) {
286
+ delete states[stateExists].isOption;
287
+ } else {
288
+ states[stateExists].isOption = state.isOption;
289
+ }
290
+
291
+ // use new `setattr` or `prop` as `setattr`
292
+ if (state.hasOwnProperty('setattr')) {
293
+ states[stateExists].setattr = state.setattr;
294
+ } else if (state.hasOwnProperty('prop')) {
295
+ states[stateExists].setattr = state.prop;
296
+ }
297
+
298
+ // remove `prop` equal to if, due to prop is uses as key in set and get
299
+ if (states[stateExists].prop === states[stateExists].id) {
300
+ delete states[stateExists].prop;
301
+ }
302
+
303
+ if (state.hasOwnProperty('epname')) {
304
+ states[stateExists].epname = state.epname;
305
+ }
306
+ states[stateExists].writable = true;
307
+ }
308
+
309
+ return states.length;
310
+
311
+ }
312
+
313
+ // search for scenes in the endpoints and build them into an array
314
+ for (const key of Object.keys(devicesMessag.endpoints || {})) {
315
+ if (devicesMessag.endpoints[key].scenes) {
316
+ scenes = scenes.concat(devicesMessag.endpoints[key].scenes);
317
+ }
318
+ }
319
+ try {
320
+ for (const expose of definition.exposes) {
321
+ let state;
322
+
323
+ switch (expose.type) {
324
+ case 'light':
325
+ for (const prop of expose.features) {
326
+ switch (prop.name) {
327
+ case 'state': {
328
+ const stateName = expose.endpoint ? `state_${expose.endpoint}` : 'state';
329
+ pushToStates(
330
+ {
331
+ id: stateName,
332
+ name: `Switch state ${expose.endpoint ? expose.endpoint : ''}`.trim(),
333
+ options: ['transition'],
334
+ icon: undefined,
335
+ role: 'switch',
336
+ write: true,
337
+ read: true,
338
+ type: 'boolean',
339
+ getter: (payload) => payload[stateName] === (prop.value_on || 'ON'),
340
+ setter: (value) =>
341
+ value
342
+ ? prop.value_on || 'ON'
343
+ : prop.value_off !== undefined
344
+ ? prop.value_off
345
+ : 'OFF',
346
+ epname: expose.endpoint,
347
+ //setattr: stateName,
348
+ },
349
+ prop.access
350
+ );
351
+ // features contains TOGGLE?
352
+ if (prop.value_toggle) {
353
+ pushToStates({
354
+ id: `${stateName}_toggle`,
355
+ prop: `${stateName}_toggle`,
356
+ name: `Toggle state of the ${stateName}`,
357
+ options: ['transition'],
358
+ icon: undefined,
359
+ role: 'button',
360
+ write: true,
361
+ read: true,
362
+ type: 'boolean',
363
+ def: true,
364
+ setattr: stateName,
365
+ setter: (value) => (value ? prop.value_toggle : undefined),
366
+ });
367
+ }
368
+ break;
369
+ }
370
+ case 'brightness': {
371
+ const stateName = expose.endpoint ? `brightness_${expose.endpoint}` : 'brightness';
372
+ pushToStates(
373
+ {
374
+ id: stateName,
375
+ name: `Brightness ${expose.endpoint ? expose.endpoint : ''}`.trim(),
376
+ options: ['transition'],
377
+ icon: undefined,
378
+ role: 'level.dimmer',
379
+ write: true,
380
+ read: true,
381
+ type: 'number',
382
+ min: 0, // ignore expose.value_min
383
+ max: 100, // ignore expose.value_max
384
+ def: 100,
385
+ unit: '%',
386
+ getter: (payload) => {
387
+ return utils.bulbLevelToAdapterLevel(payload[stateName]);
388
+ },
389
+ setter: (value) => {
390
+ return utils.adapterLevelToBulbLevel(value);
391
+ },
392
+ },
393
+ prop.access
394
+ );
395
+ // brightnessMoveOnOff
396
+ const brmPropName =
397
+ config.brightnessMoveOnOff === true
398
+ ? `${stateName}_move_onoff`
399
+ : `${stateName}_move`;
400
+ pushToStates(
401
+ {
402
+ id: `${stateName}_move`,
403
+ prop: brmPropName,
404
+ name: 'Increases or decreases the brightness by X units per second',
405
+ icon: undefined,
406
+ role: 'state',
407
+ write: true,
408
+ read: false,
409
+ type: 'number',
410
+ min: -50,
411
+ max: 50,
412
+ def: 0,
413
+ },
414
+ z2mAccess.SET
415
+ );
416
+ // brightnessStepOnOff
417
+ const brspropName =
418
+ config.brightnessStepOnOff === true
419
+ ? `${stateName}_step_onoff`
420
+ : `${stateName}_step`;
421
+ pushToStates(
422
+ {
423
+ id: `${stateName}_step`,
424
+ prop: brspropName,
425
+ name: 'Increases or decreases brightness by X steps',
426
+ options: ['transition'],
427
+ icon: undefined,
428
+ role: 'state',
429
+ write: true,
430
+ read: false,
431
+ type: 'number',
432
+ min: -255,
433
+ max: 255,
434
+ def: 0,
435
+ },
436
+ z2mAccess.SET
437
+ );
438
+ break;
439
+ }
440
+ case 'color_temp': {
441
+ const stateName = expose.endpoint ? `colortemp_${expose.endpoint}` : 'colortemp';
442
+ const propName = expose.endpoint ? `color_temp_${expose.endpoint}` : 'color_temp';
443
+ const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
444
+ pushToStates(
445
+ {
446
+ id: stateName,
447
+ prop: propName,
448
+ name: `Color temperature ${expose.endpoint ? expose.endpoint : ''}`.trim(),
449
+ options: ['transition'],
450
+ icon: undefined,
451
+ role: 'level.color.temperature',
452
+ write: true,
453
+ read: true,
454
+ type: 'number',
455
+ min:
456
+ config.useKelvin === true
457
+ ? utils.miredKelvinConversion(prop.value_max)
458
+ : prop.value_min,
459
+ max:
460
+ config.useKelvin === true
461
+ ? utils.miredKelvinConversion(prop.value_min)
462
+ : prop.value_max,
463
+ def:
464
+ config.useKelvin === true
465
+ ? utils.miredKelvinConversion(prop.value_min)
466
+ : prop.value_max,
467
+ unit: config.useKelvin === true ? 'K' : 'mired',
468
+ setter: (value) => {
469
+ return utils.toMired(value);
470
+ },
471
+ getter: (payload) => {
472
+ if (payload[colorMode] !== 'color_temp') {
473
+ return undefined;
474
+ }
475
+ // Fix 10: null-Check vor miredKelvinConversion
476
+ const val = payload[propName];
477
+ if (val == null) {return undefined;}
478
+ if (config.useKelvin === true) {
479
+ return utils.miredKelvinConversion(val);
480
+ }
481
+ return val;
482
+ },
483
+ },
484
+ prop.access
485
+ );
486
+ // Colortemp move – nur schreibbar (SET), nicht readable
487
+ pushToStates(
488
+ {
489
+ id: `${stateName}_move`,
490
+ prop: `${propName}_move`,
491
+ name: 'Colortemp change',
492
+ icon: undefined,
493
+ role: 'state',
494
+ write: true,
495
+ read: false,
496
+ type: 'number',
497
+ min: -50,
498
+ max: 50,
499
+ def: 0,
500
+ },
501
+ z2mAccess.SET // FIX: war prop.access → konnte fälschlicherweise readable werden
502
+ );
503
+ break;
504
+ }
505
+ case 'color_temp_startup': {
506
+ //const stateName = expose.endpoint
507
+ // ? `colortempstartup_${expose.endpoint}`
508
+ // : 'colortempstartup';
509
+ const propName = expose.endpoint
510
+ ? `color_temp_startup_${expose.endpoint}`
511
+ : 'color_temp_startup';
512
+ //const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
513
+ pushToStates(
514
+ {
515
+ id: propName,
516
+ prop: propName,
517
+ name: `${prop.description} ${expose.endpoint ? `(${expose.endpoint})` : ''}`.trim(),
518
+ //options: ['transition'],
519
+ icon: undefined,
520
+ role: 'level', // Changed role to level to avoid double level.temperature in one device
521
+ write: true,
522
+ read: true,
523
+ type: 'number',
524
+ min: 0,
525
+ max: 65535,
526
+ def: undefined,
527
+ unit: config.useKelvin === true ? 'K' : 'mired',
528
+ setter: (value) => {
529
+ return utils.toMired(value);
530
+ },
531
+ getter: (payload) => {
532
+ const val = payload[propName];
533
+ if (val == null) {
534
+ return undefined;
535
+ }
536
+ if (config.useKelvin === true) {
537
+ return utils.miredKelvinConversion(val);
538
+ }
539
+ return val;
540
+ },
541
+ },
542
+ prop.access
543
+ );
544
+ break;
545
+ }
546
+ case 'color_xy': {
547
+ const stateName = expose.endpoint ? `color_${expose.endpoint}` : 'color';
548
+ const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
549
+ pushToStates(
550
+ {
551
+ id: stateName,
552
+ name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
553
+ options: ['transition'],
554
+ icon: undefined,
555
+ role: 'level.color.rgb',
556
+ write: true,
557
+ read: true,
558
+ type: 'string',
559
+ def: '#ff00ff',
560
+ setter: (value) => {
561
+ // Fix 1: redundante [0,0]-Initialisierung entfernt
562
+ const rgbcolor = colors.ParseColor(value);
563
+ const xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
564
+ return {
565
+ x: xy[0],
566
+ y: xy[1],
567
+ };
568
+ },
569
+ getter: (payload) => {
570
+ if (payload[colorMode] !== 'xy' && !config.colorTempSyncColor) {
571
+ return undefined;
572
+ }
573
+ // Fix 2: x=0/y=0 sind gültige CIE-Koordinaten → != null
574
+ if (
575
+ payload[stateName] &&
576
+ payload[stateName].x != null &&
577
+ payload[stateName].y != null
578
+ ) {
579
+ const colorval = rgb.cie_to_rgb(
580
+ payload[stateName].x,
581
+ payload[stateName].y
582
+ );
583
+ return (
584
+ `#${utils.decimalToHex(colorval[0])
585
+ }${utils.decimalToHex(colorval[1])
586
+ }${utils.decimalToHex(colorval[2])}`
587
+ );
588
+ }
589
+ return undefined;
590
+ },
591
+ epname: expose.endpoint,
592
+ },
593
+ prop.access
594
+ );
595
+ break;
596
+ }
597
+ case 'color_hs': {
598
+ const stateName = expose.endpoint ? `color_${expose.endpoint}` : 'color';
599
+ const colorMode = expose.endpoint ? `color_mode_${expose.endpoint}` : 'color_mode';
600
+ pushToStates(
601
+ {
602
+ id: stateName,
603
+ name: `Color ${expose.endpoint ? expose.endpoint : ''}`.trim(),
604
+ options: ['transition'],
605
+ icon: undefined,
606
+ role: 'level.color.rgb',
607
+ write: true,
608
+ read: true,
609
+ type: 'string',
610
+ def: '#ff00ff',
611
+ setter: (value) => {
612
+ const _rgb = colors.ParseColor(value);
613
+ const hsv = rgb.rgbToHSV(_rgb.r, _rgb.g, _rgb.b, true);
614
+ return {
615
+ h: Math.min(Math.max(hsv.h, 1), 359),
616
+ s: hsv.s,
617
+ //b: Math.round(hsv.v * 2.55),
618
+ };
619
+ },
620
+ getter: (payload) => {
621
+ if (
622
+ !['hs', 'xy'].includes(payload[colorMode]) &&
623
+ !config.colorTempSyncColor
624
+ ) {
625
+ return undefined;
626
+ }
627
+ // Fix 3: h=0 (Rot) ist falsy → != null verwenden
628
+ if (
629
+ payload[stateName] &&
630
+ payload[stateName].h != null &&
631
+ payload[stateName].s != null &&
632
+ payload[stateName].b != null
633
+ ) {
634
+ return rgb.hsvToRGBString(
635
+ payload[stateName].h,
636
+ payload[stateName].s,
637
+ Math.round(payload[stateName].b / 2.55)
638
+ );
639
+ }
640
+ if (
641
+ payload[stateName] &&
642
+ payload[stateName].x != null &&
643
+ payload[stateName].y != null
644
+ ) {
645
+ const colorval = rgb.cie_to_rgb(
646
+ payload[stateName].x,
647
+ payload[stateName].y
648
+ );
649
+ return (
650
+ `#${utils.decimalToHex(colorval[0])
651
+ }${utils.decimalToHex(colorval[1])
652
+ }${utils.decimalToHex(colorval[2])}`
653
+ );
654
+ }
655
+ return undefined;
656
+ },
657
+ },
658
+ prop.access
659
+ );
660
+ break;
661
+ }
662
+ default:
663
+ pushToStates(genState(prop), prop.access);
664
+ break;
665
+ }
666
+ }
667
+ pushToStates(statesDefs.transition, z2mAccess.SET);
668
+ break;
669
+
670
+ case 'switch':
671
+ for (const prop of expose.features) {
672
+ switch (prop.name) {
673
+ case 'state':
674
+ pushToStates(genState(prop, 'switch'), prop.access);
675
+ // features contains TOGGLE?
676
+ if (prop.value_toggle) {
677
+ pushToStates({
678
+ id: `${prop.property}_toggle`,
679
+ prop: `${prop.property}_toggle`,
680
+ name: `Toggle state of the ${prop.property}`,
681
+ icon: undefined,
682
+ role: 'button',
683
+ write: true,
684
+ read: true,
685
+ type: 'boolean',
686
+ def: true,
687
+ setattr: prop.property,
688
+ setter: (value) => (value ? prop.value_toggle : undefined),
689
+ });
690
+ }
691
+ break;
692
+ default:
693
+ pushToStates(genState(prop), prop.access);
694
+ break;
695
+ }
696
+ }
697
+ break;
698
+
699
+ case 'numeric':
700
+ if (expose.endpoint) {
701
+ state = genState(expose);
702
+ } else {
703
+ switch (expose.name) {
704
+ case 'linkquality':
705
+ state = statesDefs.link_quality;
706
+ break;
707
+
708
+ case 'battery':
709
+ state = statesDefs.battery;
710
+ break;
711
+
712
+ case 'temperature':
713
+ state = statesDefs.temperature;
714
+ break;
715
+
716
+ case 'device_temperature':
717
+ state = statesDefs.device_temperature;
718
+ break;
719
+
720
+ case 'humidity':
721
+ state = statesDefs.humidity;
722
+ break;
723
+
724
+ case 'pressure':
725
+ state = statesDefs.pressure;
726
+ break;
727
+
728
+ case 'illuminance':
729
+ // Z2M sendet 'illuminance' = Rohwert (kein Lux)
730
+ // Wir legen BEIDE States an:
731
+ // 1. illuminance_raw – der unveränderte Rohwert
732
+ // 2. illuminance – in Lux konvertiert (10^(raw/10000))
733
+ // Damit bleiben bestehende Scripts auf illuminance_raw kompatibel
734
+ // und neue Scripts können direkt illuminance (Lux) nutzen.
735
+ pushToStates(statesDefs.illuminance_raw, expose.access);
736
+ state = statesDefs.illuminance_from_raw;
737
+ break;
738
+
739
+ case 'illuminance_raw':
740
+ // Gerät sendet payload.illuminance_raw direkt (eigener Expose-Typ)
741
+ // Wir legen ebenfalls BEIDE States an:
742
+ // 1. illuminance_raw – der unveränderte Rohwert (prop='illuminance_raw')
743
+ // 2. illuminance – in Lux konvertiert (10^(raw/10000))
744
+ pushToStates(statesDefs.illuminance_raw_direct, expose.access);
745
+ state = statesDefs.illuminance_from_illuminance_raw;
746
+ break;
747
+
748
+ case 'illuminance_lux':
749
+ state = statesDefs.illuminance;
750
+ break;
751
+
752
+ case 'power':
753
+ state = statesDefs.load_power;
754
+ break;
755
+
756
+ case 'current':
757
+ state = statesDefs.load_current;
758
+ break;
759
+
760
+ case 'voltage':
761
+ // FIX: Klone vor Mutation, damit statesDefs.voltage nicht dauerhaft verändert wird
762
+ state = Object.assign({}, power_source === 'Battery' ? statesDefs.battery_voltage : statesDefs.voltage);
763
+ if (expose.unit === 'mV') {
764
+ // Fix 7: expose.property statt hardcoded 'voltage'
765
+ const voltProp = expose.property;
766
+ state.getter = (payload) => payload[voltProp] != null ? payload[voltProp] / 1000 : undefined;
767
+ }
768
+ break;
769
+
770
+ case 'energy':
771
+ state = statesDefs.energy;
772
+ break;
773
+
774
+ default:
775
+ state = genState(expose);
776
+ break;
777
+ }
778
+ }
779
+ if (state) {pushToStates(state, expose.access);}
780
+ break;
781
+
782
+ case 'enum':
783
+ switch (expose.name) {
784
+ case 'action': {
785
+ // generate an 'action' state
786
+ state = genState(expose);
787
+ state.isEvent = true;
788
+ state.getter = (payload) => payload.action;
789
+ pushToStates(state, expose.access);
790
+ state = null;
791
+
792
+ if (!Array.isArray(expose.values)) {
793
+ break;
794
+ }
795
+
796
+ // Support for DIYRuZ Device
797
+ const wildcardValues = expose.values.filter((x) => x.startsWith('*'));
798
+ if (wildcardValues && wildcardValues.length > 0) {
799
+ for (const endpointName of [
800
+ ...new Set(definition.exposes.filter((x) => x.endpoint).map((x) => x.endpoint)),
801
+ ]) {
802
+ for (const value of wildcardValues) {
803
+ const actionName = value.replace('*', endpointName);
804
+ pushToStates(
805
+ {
806
+ id: actionName,
807
+ prop: 'action',
808
+ name: `Triggered action ${value.replace('*_', endpointName)}`,
809
+ icon: undefined,
810
+ role: 'button',
811
+ write: false,
812
+ read: true,
813
+ type: 'boolean',
814
+ def: false,
815
+ isEvent: true,
816
+ getter: (payload) => (payload.action === actionName ? true : undefined),
817
+ },
818
+ expose.access
819
+ );
820
+ }
821
+ }
822
+ break;
823
+ }
824
+
825
+ for (const actionName of expose.values) {
826
+ // is release -> hold state? - skip
827
+ if (
828
+ config.simpleHoldReleaseState === true &&
829
+ actionName.endsWith('release') &&
830
+ expose.values.find((x) => x === actionName.replace('release', 'hold'))
831
+ ) {
832
+ continue;
833
+ }
834
+
835
+ // is stop - move state? - skip
836
+ if (
837
+ config.simpleMoveStopState === true &&
838
+ actionName.endsWith('stop') &&
839
+ expose.values.find((x) => x.includes(actionName.replace('stop', 'move')))
840
+ ) {
841
+ continue;
842
+ }
843
+
844
+ // is release -> press state? - skip
845
+ if (
846
+ config.simplePressReleaseState === true &&
847
+ actionName.endsWith('release') &&
848
+ expose.values.find((x) => x === actionName.replace('release', 'press'))
849
+ ) {
850
+ continue;
851
+ }
852
+
853
+ // is hold -> release state ?
854
+ if (
855
+ config.simpleHoldReleaseState === true &&
856
+ actionName.endsWith('hold') &&
857
+ expose.values.find((x) => x === actionName.replace('hold', 'release'))
858
+ ) {
859
+ pushToStates(
860
+ {
861
+ id: actionName.replace(/\*/g, ''),
862
+ prop: 'action',
863
+ name: actionName,
864
+ icon: undefined,
865
+ role: 'button',
866
+ write: false,
867
+ read: true,
868
+ def: false,
869
+ type: 'boolean',
870
+ getter: (payload) => {
871
+ if (payload.action === actionName) {
872
+ return true;
873
+ }
874
+ if (payload.action === actionName.replace('hold', 'release')) {
875
+ return false;
876
+ }
877
+ if (payload.action === `${actionName}_release`) {
878
+ return false;
879
+ }
880
+ return undefined;
881
+ },
882
+ },
883
+ expose.access
884
+ );
885
+ }
886
+ // is move -> stop state ?
887
+ else if (
888
+ config.simpleMoveStopState === true &&
889
+ actionName.includes('move') &&
890
+ expose.values.find((x) => x === `${actionName.split('_')[0]}_stop`)
891
+ ) {
892
+ pushToStates(
893
+ {
894
+ id: actionName.replace(/\*/g, ''),
895
+ prop: 'action',
896
+ name: actionName,
897
+ icon: undefined,
898
+ role: 'button',
899
+ write: false,
900
+ read: true,
901
+ def: false,
902
+ type: 'boolean',
903
+ getter: (payload) => {
904
+ if (payload.action === actionName) {
905
+ return true;
906
+ }
907
+ if (payload.action === `${actionName.split('_')[0]}_stop`) {
908
+ return false;
909
+ }
910
+ return undefined;
911
+ },
912
+ },
913
+ expose.access
914
+ );
915
+ }
916
+ // is press -> release state ?
917
+ else if (
918
+ config.simplePressReleaseState === true &&
919
+ actionName.endsWith('press') &&
920
+ expose.values.find((x) => x === actionName.replace('press', 'release'))
921
+ ) {
922
+ pushToStates(
923
+ {
924
+ id: actionName.replace(/\*/g, ''),
925
+ prop: 'action',
926
+ name: actionName,
927
+ icon: undefined,
928
+ role: 'button',
929
+ write: false,
930
+ read: true,
931
+ def: false,
932
+ type: 'boolean',
933
+ getter: (payload) => {
934
+ if (payload.action === actionName) {
935
+ return true;
936
+ }
937
+ if (payload.action === actionName.replace('press', 'release')) {
938
+ return false;
939
+ }
940
+ return undefined;
941
+ },
942
+ },
943
+ expose.access
944
+ );
945
+ } else if (actionName === 'color_temperature_move') {
946
+ pushToStates(
947
+ {
948
+ id: 'color_temperature_move',
949
+ prop: 'action',
950
+ name: 'Color temperature move value',
951
+ icon: undefined,
952
+ role: 'level.color.temperature',
953
+ write: false,
954
+ read: true,
955
+ type: 'number',
956
+ def: config.useKelvin === true ? utils.miredKelvinConversion(150) : 500,
957
+ min: config.useKelvin === true ? utils.miredKelvinConversion(500) : 150,
958
+ max: config.useKelvin === true ? utils.miredKelvinConversion(150) : 500,
959
+ unit: config.useKelvin === true ? 'K' : 'mired',
960
+ isEvent: true,
961
+ getter: (payload) => {
962
+ if (payload.action !== 'color_temperature_move') {
963
+ return undefined;
964
+ }
965
+ // Fix 4: != null statt truthy (0 ist ein gültiger Mired-Wert)
966
+ if (payload.action_color_temperature != null) {
967
+ if (config.useKelvin === true) {
968
+ return utils.miredKelvinConversion(
969
+ payload.action_color_temperature
970
+ );
971
+ }
972
+ return payload.action_color_temperature;
973
+ }
974
+ return undefined;
975
+ },
976
+ },
977
+ expose.access
978
+ );
979
+ } else if (actionName === 'color_move') {
980
+ pushToStates(
981
+ {
982
+ id: 'color_move',
983
+ prop: 'action',
984
+ name: 'Color move value',
985
+ icon: undefined,
986
+ role: 'level.color.rgb',
987
+ write: false,
988
+ read: true,
989
+ type: 'string',
990
+ def: '#ffffff',
991
+ isEvent: true,
992
+ getter: (payload) => {
993
+ if (payload.action !== 'color_move') {
994
+ return undefined;
995
+ }
996
+
997
+ if (
998
+ payload.action_color &&
999
+ payload.action_color.hasOwnProperty('x') &&
1000
+ payload.action_color.hasOwnProperty('y')
1001
+ ) {
1002
+ const colorval = rgb.cie_to_rgb(
1003
+ payload.action_color.x,
1004
+ payload.action_color.y
1005
+ );
1006
+ return (
1007
+ `#${
1008
+ utils.decimalToHex(colorval[0])
1009
+ }${utils.decimalToHex(colorval[1])
1010
+ }${utils.decimalToHex(colorval[2])}`
1011
+ );
1012
+ }
1013
+ return undefined;
1014
+
1015
+ },
1016
+ },
1017
+ expose.access
1018
+ );
1019
+ } else if (actionName === 'brightness_move_to_level') {
1020
+ pushToStates(
1021
+ {
1022
+ id: 'brightness_move_to_level',
1023
+ prop: 'action', // Fix 1: fehlte → statesController fand State nie
1024
+ name: 'Brightness move to level',
1025
+ icon: undefined,
1026
+ role: 'level.dimmer',
1027
+ write: false,
1028
+ read: true,
1029
+ type: 'number',
1030
+ min: 0,
1031
+ max: 100,
1032
+ def: 100,
1033
+ unit: '%',
1034
+ isEvent: true,
1035
+ getter: (payload) => {
1036
+ if (payload.action !== 'brightness_move_to_level') {
1037
+ return undefined;
1038
+ }
1039
+ // Fix 3: != null statt truthy (action_level=0 ist gültig)
1040
+ if (payload.action_level != null) {
1041
+ return utils.bulbLevelToAdapterLevel(payload.action_level);
1042
+ }
1043
+ return undefined;
1044
+ },
1045
+ },
1046
+ expose.access
1047
+ );
1048
+ } else if (actionName === 'move_to_saturation') {
1049
+ pushToStates(
1050
+ {
1051
+ id: 'move_to_saturation',
1052
+ prop: 'action', // Fix 2: fehlte → statesController fand State nie
1053
+ name: 'Move to level saturation',
1054
+ icon: undefined,
1055
+ role: 'level.color.saturation',
1056
+ write: false,
1057
+ read: true,
1058
+ type: 'number',
1059
+ // min: 0,
1060
+ // max: 100,
1061
+ def: 0,
1062
+ isEvent: true,
1063
+ getter: (payload) => {
1064
+ if (payload.action !== 'move_to_saturation') {
1065
+ return undefined;
1066
+ }
1067
+ // FIX: action_saturation prüfen, nicht action_level
1068
+ if (payload.action_saturation != null) {
1069
+ return payload.action_saturation;
1070
+ }
1071
+ return undefined;
1072
+ },
1073
+ },
1074
+ expose.access
1075
+ );
1076
+ } else if (actionName === 'enhanced_move_to_hue_and_saturation') {
1077
+ pushToStates(
1078
+ {
1079
+ id: 'enhanced_move_to_hue_and_saturation',
1080
+ prop: 'action',
1081
+ name: 'Enhanced move to hue and saturation value',
1082
+ icon: undefined,
1083
+ role: 'level.color.hue',
1084
+ write: false,
1085
+ read: true,
1086
+ type: 'number',
1087
+ min: 0,
1088
+ max: 65536,
1089
+ def: 0,
1090
+ isEvent: true,
1091
+ getter: (payload) => {
1092
+ if (payload.action !== 'enhanced_move_to_hue_and_saturation') {
1093
+ return undefined;
1094
+ }
1095
+ // Fix 5: != null statt truthy (0 ist gültiger Hue-Wert)
1096
+ if (payload.action_enhanced_hue != null) {
1097
+ return payload.action_enhanced_hue;
1098
+ }
1099
+ return undefined;
1100
+ },
1101
+ },
1102
+ expose.access
1103
+ );
1104
+ }
1105
+
1106
+ else {
1107
+ pushToStates(
1108
+ {
1109
+ id: actionName.replace(/\*/g, ''),
1110
+ prop: 'action',
1111
+ name: actionName,
1112
+ icon: undefined,
1113
+ role: 'button',
1114
+ write: false,
1115
+ read: true,
1116
+ type: 'boolean',
1117
+ def: false,
1118
+ isEvent: true,
1119
+ getter: (payload) => (payload.action === actionName ? true : undefined),
1120
+ },
1121
+ expose.access
1122
+ );
1123
+ }
1124
+ }
1125
+ // Can the device simulated_brightness?
1126
+ if (
1127
+ definition.options &&
1128
+ definition.options.find((x) => x.property === 'simulated_brightness')
1129
+ ) {
1130
+ pushToStates(statesDefs.simulated_brightness, z2mAccess.STATE);
1131
+ }
1132
+ state = null;
1133
+ break;
1134
+ }
1135
+ default:
1136
+ state = genState(expose);
1137
+ break;
1138
+ }
1139
+ if (state) {pushToStates(state, expose.access);}
1140
+ break;
1141
+
1142
+ case 'binary':
1143
+ if (expose.endpoint) {
1144
+ state = genState(expose);
1145
+ } else {
1146
+ switch (expose.name) {
1147
+ case 'contact':
1148
+ state = statesDefs.contact;
1149
+ pushToStates(statesDefs.opened, expose.access);
1150
+ break;
1151
+
1152
+ case 'battery_low':
1153
+ state = statesDefs.batt_low_t_f;
1154
+ break;
1155
+
1156
+ case 'tamper':
1157
+ state = statesDefs.tamper;
1158
+ break;
1159
+
1160
+ case 'water_leak':
1161
+ state = statesDefs.water_leak;
1162
+ break;
1163
+
1164
+ case 'lock':
1165
+ state = statesDefs.child_lock;
1166
+ break;
1167
+
1168
+ case 'occupancy':
1169
+ state = statesDefs.occupancy;
1170
+ break;
1171
+
1172
+ default:
1173
+ state = genState(expose);
1174
+ break;
1175
+ }
1176
+ }
1177
+ if (state) {pushToStates(state, expose.access);}
1178
+ break;
1179
+
1180
+ case 'list':
1181
+ // Z2M 'list' type Array-Wert als JSON-String speichern
1182
+ pushToStates({
1183
+ id: expose.property,
1184
+ prop: expose.property,
1185
+ name: expose.description || expose.name || expose.property,
1186
+ icon: undefined,
1187
+ role: 'json',
1188
+ write: (expose.access & z2mAccess.SET) > 0,
1189
+ read: true,
1190
+ type: 'string',
1191
+ def: '[]',
1192
+ getter: (payload) => {
1193
+ const val = payload[expose.property];
1194
+ if (val == null) {return undefined;}
1195
+ return typeof val === 'string' ? val : JSON.stringify(val);
1196
+ },
1197
+ }, expose.access);
1198
+ break;
1199
+
1200
+ case 'text':
1201
+ state = genState(expose);
1202
+ pushToStates(state, expose.access);
1203
+ break;
1204
+
1205
+ case 'lock':
1206
+ case 'fan':
1207
+ case 'cover':
1208
+ for (const prop of expose.features) {
1209
+ switch (prop.name) {
1210
+ case 'state':
1211
+ pushToStates(genState(prop, 'switch'), prop.access);
1212
+ // features contains TOGGLE?
1213
+ if (prop.value_toggle) {
1214
+ pushToStates({
1215
+ id: `${prop.property}_toggle`,
1216
+ prop: `${prop.property}_toggle`,
1217
+ name: `Toggle state of the ${prop.property}`,
1218
+ icon: undefined,
1219
+ role: 'button',
1220
+ write: true,
1221
+ read: true,
1222
+ def: true,
1223
+ type: 'boolean',
1224
+ setattr: prop.property,
1225
+ setter: (value) => (value ? prop.value_toggle : undefined),
1226
+ });
1227
+ }
1228
+ break;
1229
+ default:
1230
+ pushToStates(genState(prop), prop.access);
1231
+ break;
1232
+ }
1233
+ }
1234
+ break;
1235
+
1236
+ case 'climate':
1237
+ for (const prop of expose.features) {
1238
+ switch (prop.name) {
1239
+ case 'away_mode':
1240
+ pushToStates(statesDefs.climate_away_mode, prop.access);
1241
+ break;
1242
+ case 'system_mode':
1243
+ pushToStates(statesDefs.climate_system_mode, prop.access);
1244
+ break;
1245
+ case 'running_mode':
1246
+ case 'running_state': // Z2M nutzt beide Bezeichnungen je nach Version
1247
+ pushToStates(statesDefs.climate_running_mode, prop.access);
1248
+ break;
1249
+ case 'local_temperature':
1250
+ pushToStates(statesDefs.local_temperature, prop.access);
1251
+ break;
1252
+ case 'local_temperature_calibration':
1253
+ pushToStates(statesDefs.local_temperature_calibration, prop.access);
1254
+ break;
1255
+ default:
1256
+ {
1257
+ if (prop.name.includes('heating_setpoint')) {
1258
+ pushToStates(genState(prop, 'level.temperature'), prop.access);
1259
+ } else {
1260
+ pushToStates(genState(prop), prop.access);
1261
+ }
1262
+ }
1263
+ break;
1264
+ }
1265
+ }
1266
+ break;
1267
+
1268
+ case 'composite': {
1269
+ for (const propRaw of expose.features) {
1270
+ // FIX: Clone um Mutation des originalen Z2M-Expose-Objekts zu vermeiden
1271
+ // (prop.type = 'text' würde sonst beim nächsten bridge/devices-Event noch 'text' sein)
1272
+ const prop = Object.assign({}, propRaw);
1273
+ prop.type = 'text'; // to avoid problems with numbers, booleans, etc.
1274
+
1275
+ const state = genState(prop);
1276
+ // Workaround for FP1 new state (region_upsert)
1277
+ if (!state) {
1278
+ continue;
1279
+ }
1280
+
1281
+ state.prop = expose.property;
1282
+ state.inOptions = true;
1283
+ state.isOption = true;
1284
+
1285
+ if (expose.access & z2mAccess.STATE) {
1286
+ state.getter = (payload) => {
1287
+ if (
1288
+ payload.hasOwnProperty(expose.property) &&
1289
+ payload[expose.property] !== null &&
1290
+ payload[expose.property].hasOwnProperty(prop.property)
1291
+ ) {
1292
+ return !isNaN(payload[expose.property][prop.property])
1293
+ ? payload[expose.property][prop.property]
1294
+ : undefined;
1295
+ }
1296
+ return undefined;
1297
+
1298
+ };
1299
+ } else {
1300
+ state.getter = (payload) => {
1301
+ // Fix: null-Check vor Zugriff auf property
1302
+ if (!payload[expose.property]) {
1303
+ return undefined;
1304
+ }
1305
+ return payload[expose.property][prop.property];
1306
+ };
1307
+ }
1308
+
1309
+ pushToStates(state, z2mAccess.STATE);
1310
+ }
1311
+
1312
+ break;
1313
+ }
1314
+ default:
1315
+ adapter.log.debug(`Unhandled expose type ${expose.type} for device ${deviceID}`);
1316
+ }
1317
+ }
1318
+ } catch (err) {
1319
+ adapter.log.error(`ERROR in expose for device ${deviceID} : ${err}`);
1320
+ }
1321
+
1322
+ // Fix 8: definition.model kann undefined sein → Fallback auf ''
1323
+ for (const state of getNonGenDevStatesDefs(definition.model || '')) {
1324
+ pushToStates(state, state.write ? z2mAccess.SET : z2mAccess.STATE);
1325
+ }
1326
+
1327
+ // Add default states
1328
+ pushToStates(statesDefs.available, z2mAccess.STATE);
1329
+ pushToStates(statesDefs.last_seen, z2mAccess.STATE);
1330
+ pushToStates(statesDefs.send_payload, z2mAccess.SET);
1331
+
1332
+ // Create buttons for scenes
1333
+ for (const scene of scenes) {
1334
+ // Fix 9: scene.name kann null/undefined sein
1335
+ pushToStates({
1336
+ id: `scene_${scene.id}`,
1337
+ prop: `scene_recall`,
1338
+ name: scene.name || `Scene ${scene.id}`,
1339
+ icon: undefined,
1340
+ role: 'button',
1341
+ write: true,
1342
+ read: true,
1343
+ def: true,
1344
+ type: 'boolean',
1345
+ setter: (value) => (value ? scene.id : undefined),
1346
+ });
1347
+ }
1348
+
1349
+ return {
1350
+ id: deviceID,
1351
+ ieee_address: ieee_address,
1352
+ power_source: power_source,
1353
+ disabled: disabled,
1354
+ description: description,
1355
+ optionsValues: {},
1356
+ states: states,
1357
+ };
1358
+ }
1359
+
1360
+ module.exports = {
1361
+ createDeviceFromExposes: createDeviceFromExposes,
1362
+ };