iobroker.zigbee2mqtt 3.1.5 → 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/README.md +10 -81
- package/io-package.json +66 -66
- package/lib/deviceController.js +2 -2
- package/lib/exposes.js +1362 -1362
- package/lib/mqttServerController.js +22 -22
- package/lib/states.js +6 -2
- package/lib/statesController.js +4 -4
- package/lib/websocketController.js +1 -1
- package/lib/z2mController.js +3 -3
- package/main.js +26 -17
- package/package.json +2 -2
package/lib/exposes.js
CHANGED
|
@@ -1,1362 +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
|
-
// 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
|
-
};
|
|
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
|
+
};
|