iobroker.zigbee2mqtt 3.0.21 → 3.1.5
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 +23 -0
- package/admin/jsonConfig.json +37 -0
- package/io-package.json +68 -68
- package/lib/check.js +24 -14
- package/lib/colors.js +9 -9
- package/lib/deviceController.js +244 -157
- package/lib/exposes.js +1362 -1315
- package/lib/imageController.js +98 -66
- package/lib/messages.js +29 -19
- package/lib/mqttServerController.js +120 -23
- package/lib/nonGenericDevicesExtension.js +2 -3
- package/lib/rgb.js +52 -63
- package/lib/states.js +150 -33
- package/lib/statesController.js +152 -72
- package/lib/utils.js +37 -97
- package/lib/websocketController.js +187 -50
- package/lib/z2mController.js +63 -42
- package/main.js +467 -201
- package/package.json +13 -12
package/lib/statesController.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const utils = require('./utils');
|
|
2
|
-
const incStatsQueue = [];
|
|
3
|
-
const timeOutCache = {};
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Verwaltet das Schreiben von Zigbee2MQTT-Gerätedaten in ioBroker-States.
|
|
7
|
+
* Puffert eingehende Nachrichten für noch nicht erstellte Geräte in einer Queue.
|
|
7
8
|
*/
|
|
8
9
|
class StatesController {
|
|
9
10
|
/**
|
|
11
|
+
* Erstellt eine neue StatesController-Instanz.
|
|
10
12
|
*
|
|
11
|
-
* @param adapter
|
|
12
|
-
* @param deviceCache
|
|
13
|
-
* @param groupCache
|
|
14
|
-
* @param logCustomizations
|
|
15
|
-
* @param createCache
|
|
13
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
14
|
+
* @param {Array} deviceCache Gemeinsamer Cache aller bekannten Geräte
|
|
15
|
+
* @param {Array} groupCache Gemeinsamer Cache aller bekannten Gruppen
|
|
16
|
+
* @param {object} logCustomizations Debug/Filter-Einstellungen (debugDevices, logfilter)
|
|
17
|
+
* @param {object} createCache Cache bereits erstellter ioBroker-Objekte
|
|
16
18
|
*/
|
|
17
19
|
constructor(adapter, deviceCache, groupCache, logCustomizations, createCache) {
|
|
18
20
|
this.adapter = adapter;
|
|
@@ -20,45 +22,99 @@ class StatesController {
|
|
|
20
22
|
this.deviceCache = deviceCache;
|
|
21
23
|
this.logCustomizations = logCustomizations;
|
|
22
24
|
this.createCache = createCache;
|
|
25
|
+
this.incStatsQueue = [];
|
|
26
|
+
this.timeOutCache = {};
|
|
27
|
+
// Einmalig berechnen – wird nur bei Konfigurationsänderung ungültig
|
|
28
|
+
this._debugDeviceList = logCustomizations.debugDevices
|
|
29
|
+
? String(logCustomizations.debugDevices).split(',').map((s) => s.trim()).filter(Boolean)
|
|
30
|
+
: [];
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
/**
|
|
34
|
+
* Verarbeitet eine eingehende Gerätenachricht von Zigbee2MQTT.
|
|
35
|
+
* Ist das Gerät noch nicht im Cache bekannt, wird die Nachricht in der incStatsQueue
|
|
36
|
+
* gepuffert und später über processQueue() erneut versucht.
|
|
26
37
|
*
|
|
27
|
-
* @param messageObj
|
|
38
|
+
* @param {{ topic: string, payload: any }} messageObj Die zu verarbeitende Nachricht
|
|
28
39
|
*/
|
|
29
|
-
processDeviceMessage(messageObj) {
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
async processDeviceMessage(messageObj) {
|
|
41
|
+
if (!messageObj || typeof messageObj !== 'object') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (messageObj.payload === '' || messageObj.payload === undefined || messageObj.payload === null) {
|
|
32
45
|
return;
|
|
33
46
|
}
|
|
34
47
|
|
|
35
|
-
const device = this.groupCache.concat(this.deviceCache).find((x) => x.id
|
|
48
|
+
const device = this.groupCache.concat(this.deviceCache).find((x) => x.id === messageObj.topic);
|
|
36
49
|
if (device) {
|
|
37
50
|
try {
|
|
38
|
-
this.setDeviceStateSafely(messageObj, device);
|
|
51
|
+
await this.setDeviceStateSafely(messageObj, device);
|
|
39
52
|
} catch (error) {
|
|
40
|
-
this.adapter.log.error(error);
|
|
53
|
+
this.adapter.log.error(`setDeviceStateSafely error for ${messageObj.topic}: ${error}`);
|
|
41
54
|
}
|
|
42
55
|
} else {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
// Wenn das Gerät (noch) nicht bekannt ist: Message in Queue stellen.
|
|
57
|
+
// Existiert für dieses Topic bereits ein Eintrag, wird er mit den aktuellen
|
|
58
|
+
// Payload-Daten überschrieben, damit wir stets den neuesten Stand verarbeiten.
|
|
59
|
+
const existingIdx = this.incStatsQueue.findIndex((x) => x && x.topic === messageObj.topic);
|
|
60
|
+
if (existingIdx !== -1) {
|
|
61
|
+
const ttl = (this.incStatsQueue[existingIdx]._ttl || 0) + 1;
|
|
62
|
+
if (ttl > 10) {
|
|
63
|
+
this.adapter.log.warn(`incStatsQueue: dropping message for unknown device ${messageObj.topic} after ${ttl} retries`);
|
|
64
|
+
this.incStatsQueue.splice(existingIdx, 1);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.incStatsQueue[existingIdx] = { ...messageObj, _ttl: ttl };
|
|
68
|
+
} else {
|
|
69
|
+
if (this.incStatsQueue.length < 500) {
|
|
70
|
+
this.incStatsQueue.push({ ...messageObj, _ttl: 1 });
|
|
71
|
+
} else {
|
|
72
|
+
this.adapter.log.warn(`incStatsQueue is full (500), dropping message for ${messageObj.topic}`);
|
|
73
|
+
}
|
|
46
74
|
}
|
|
47
75
|
this.adapter.log.debug(`Device: ${messageObj.topic} not found, queue state in incStatsQueue!`);
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
|
|
51
79
|
/**
|
|
80
|
+
* Schreibt alle State-Werte einer Nachricht in die zugehörigen ioBroker-States.
|
|
81
|
+
* Action-States werden gesammelt und am Ende gesondert behandelt.
|
|
52
82
|
*
|
|
53
|
-
* @param messageObj
|
|
54
|
-
* @param device
|
|
83
|
+
* @param {{ topic: string, payload: object }} messageObj Die zu verarbeitende Nachricht
|
|
84
|
+
* @param {object} device Das zugehörige Geräteobjekt aus dem Cache
|
|
55
85
|
*/
|
|
56
86
|
async setDeviceStateSafely(messageObj, device) {
|
|
57
|
-
if (this.
|
|
87
|
+
if (this._debugDeviceList.includes(device.ieee_address)) {
|
|
58
88
|
this.adapter.log.warn(`--->>> fromZ2M -> ${device.ieee_address} states: ${JSON.stringify(messageObj)}`);
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
const actionStates = [];
|
|
92
|
+
// Fix 1: Flag damit messageObj nur EINMAL in die Queue kommt, egal wie viele
|
|
93
|
+
// States noch nicht im createCache sind (verhindert N-faches Requeue)
|
|
94
|
+
let queuedThisRound = false;
|
|
95
|
+
|
|
96
|
+
const pushToQueue = (msg) => {
|
|
97
|
+
if (queuedThisRound) {return;}
|
|
98
|
+
const existingIdx = this.incStatsQueue.findIndex((x) => x && x.topic === msg.topic);
|
|
99
|
+
if (existingIdx !== -1) {
|
|
100
|
+
const ttl = (this.incStatsQueue[existingIdx]._ttl || 0) + 1;
|
|
101
|
+
if (ttl > 10) {
|
|
102
|
+
this.adapter.log.warn(`incStatsQueue: dropping message for ${msg.topic} after ${ttl} retries (state not yet created)`);
|
|
103
|
+
this.incStatsQueue.splice(existingIdx, 1);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.incStatsQueue[existingIdx] = { ...msg, _ttl: ttl };
|
|
107
|
+
} else if (this.incStatsQueue.length < 500) {
|
|
108
|
+
this.incStatsQueue.push({ ...msg, _ttl: 1 });
|
|
109
|
+
} else {
|
|
110
|
+
this.adapter.log.warn(`incStatsQueue is full, dropping message for ${msg.topic}`);
|
|
111
|
+
}
|
|
112
|
+
queuedThisRound = true;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (!messageObj.payload || typeof messageObj.payload !== 'object' || Array.isArray(messageObj.payload)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
62
118
|
|
|
63
119
|
for (let [key, value] of Object.entries(messageObj.payload)) {
|
|
64
120
|
if (value === undefined || value === null) {
|
|
@@ -69,12 +125,12 @@ class StatesController {
|
|
|
69
125
|
return state.prop && state.prop === key;
|
|
70
126
|
});
|
|
71
127
|
|
|
72
|
-
if (states.length
|
|
73
|
-
states = device.states.filter((x) => x.id
|
|
128
|
+
if (states.length === 0) {
|
|
129
|
+
states = device.states.filter((x) => x.id === key);
|
|
74
130
|
}
|
|
75
131
|
|
|
76
|
-
if (states.length
|
|
77
|
-
if (key
|
|
132
|
+
if (states.length === 0) {
|
|
133
|
+
if (key === 'device' || device.ieee_address.includes('group')) {
|
|
78
134
|
// do nothing
|
|
79
135
|
} else {
|
|
80
136
|
// some devices has addition information in payload
|
|
@@ -92,16 +148,16 @@ class StatesController {
|
|
|
92
148
|
common: {
|
|
93
149
|
name: key,
|
|
94
150
|
role: 'state',
|
|
95
|
-
type:
|
|
151
|
+
type: typeof value,
|
|
96
152
|
write: false,
|
|
97
153
|
read: true,
|
|
98
154
|
},
|
|
99
155
|
native: {},
|
|
100
156
|
});
|
|
101
|
-
if (typeof value
|
|
157
|
+
if (typeof value === 'object') {
|
|
102
158
|
value = JSON.stringify(value);
|
|
103
159
|
}
|
|
104
|
-
this.adapter.
|
|
160
|
+
await this.adapter.setStateAsync(`${fullPath}.${key}`, value, true);
|
|
105
161
|
}
|
|
106
162
|
continue;
|
|
107
163
|
}
|
|
@@ -114,26 +170,22 @@ class StatesController {
|
|
|
114
170
|
await this.setStateSafelyAsync(`${device.ieee_address}.available`, true);
|
|
115
171
|
}
|
|
116
172
|
|
|
117
|
-
//
|
|
173
|
+
// State noch nicht erstellt? → einmal in Queue legen und weiter
|
|
118
174
|
if (!this.createCache[device.ieee_address]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
|| !this.createCache[device.ieee_address][state.id]
|
|
176
|
+
|| this.createCache[device.ieee_address][state.id].created !== true) {
|
|
177
|
+
pushToQueue(messageObj);
|
|
122
178
|
continue;
|
|
123
179
|
}
|
|
124
180
|
|
|
125
181
|
try {
|
|
126
182
|
// Is an action
|
|
127
|
-
if (state.prop && state.prop
|
|
183
|
+
if (state.prop && state.prop === 'action') {
|
|
128
184
|
actionStates.push(state);
|
|
129
185
|
}
|
|
130
|
-
|
|
131
|
-
// check if its a motion sensor (occupancy state) and if configuration is set to update state every time
|
|
132
|
-
// if yes, use setStateSafelyAsync instead of setStateChangedSafelyAsync
|
|
133
|
-
else if (this.adapter.config.allwaysUpdateOccupancyState === true && state.id === 'occupancy' && value === true) {
|
|
186
|
+
else if (this.adapter.config.allwaysUpdateOccupancyState === true && state.id === 'occupancy' && value === true) {
|
|
134
187
|
await this.setStateSafelyAsync(stateName, value);
|
|
135
188
|
}
|
|
136
|
-
// end section for motion sensor update
|
|
137
189
|
else {
|
|
138
190
|
if (state.getter) {
|
|
139
191
|
await this.setStateChangedSafelyAsync(stateName, state.getter(messageObj.payload));
|
|
@@ -142,22 +194,21 @@ class StatesController {
|
|
|
142
194
|
}
|
|
143
195
|
}
|
|
144
196
|
} catch (err) {
|
|
145
|
-
|
|
146
|
-
this.adapter.log.debug(`Can not set ${stateName}, queue state in incStatsQueue!`);
|
|
197
|
+
this.adapter.log.warn(`Cannot set state ${stateName}: ${err}`);
|
|
147
198
|
}
|
|
148
199
|
}
|
|
149
200
|
}
|
|
150
201
|
|
|
202
|
+
|
|
151
203
|
for (const state of actionStates) {
|
|
152
204
|
const stateName = `${device.ieee_address}.${state.id}`;
|
|
153
205
|
|
|
154
206
|
try {
|
|
155
207
|
const getterPayload = state.getter(messageObj.payload);
|
|
156
|
-
if (getterPayload
|
|
157
|
-
if (state.isEvent && state.isEvent
|
|
158
|
-
if (state.type
|
|
208
|
+
if (getterPayload !== undefined) {
|
|
209
|
+
if (state.isEvent && state.isEvent === true) {
|
|
210
|
+
if (state.type === 'boolean') {
|
|
159
211
|
await this.setStateWithTimeoutAsync(stateName, getterPayload, 250);
|
|
160
|
-
|
|
161
212
|
} else {
|
|
162
213
|
await this.setStateSafelyAsync(stateName, getterPayload);
|
|
163
214
|
}
|
|
@@ -166,16 +217,17 @@ class StatesController {
|
|
|
166
217
|
}
|
|
167
218
|
}
|
|
168
219
|
} catch (err) {
|
|
169
|
-
|
|
170
|
-
this.adapter.log.debug(`Can not set ${stateName}, queue state in incStatsQueue!`);
|
|
220
|
+
this.adapter.log.warn(`Cannot set action state ${stateName}: ${err}`);
|
|
171
221
|
}
|
|
172
222
|
}
|
|
173
223
|
}
|
|
174
224
|
|
|
175
225
|
/**
|
|
226
|
+
* Setzt einen ioBroker-State (immer, ohne Changed-Prüfung).
|
|
227
|
+
* Ignoriert null/undefined-Werte sicher.
|
|
176
228
|
*
|
|
177
|
-
* @param stateName
|
|
178
|
-
* @param value
|
|
229
|
+
* @param {string} stateName Vollständiger State-Pfad (z.B. "0xAABB.state")
|
|
230
|
+
* @param {*} value Der zu setzende Wert
|
|
179
231
|
*/
|
|
180
232
|
async setStateSafelyAsync(stateName, value) {
|
|
181
233
|
if (value === undefined || value === null) {
|
|
@@ -185,9 +237,11 @@ class StatesController {
|
|
|
185
237
|
}
|
|
186
238
|
|
|
187
239
|
/**
|
|
240
|
+
* Setzt einen ioBroker-State nur wenn sich der Wert geändert hat.
|
|
241
|
+
* Ignoriert null/undefined-Werte sicher.
|
|
188
242
|
*
|
|
189
|
-
* @param stateName
|
|
190
|
-
* @param value
|
|
243
|
+
* @param {string} stateName Vollständiger State-Pfad (z.B. "0xAABB.brightness")
|
|
244
|
+
* @param {*} value Der zu setzende Wert
|
|
191
245
|
*/
|
|
192
246
|
async setStateChangedSafelyAsync(stateName, value) {
|
|
193
247
|
if (value === undefined || value === null) {
|
|
@@ -197,10 +251,13 @@ class StatesController {
|
|
|
197
251
|
}
|
|
198
252
|
|
|
199
253
|
/**
|
|
254
|
+
* Setzt einen State sofort auf den angegebenen Wert und – nur bei value=true –
|
|
255
|
+
* nach Ablauf des Timeouts automatisch zurück auf false (Button/Event-Reset).
|
|
256
|
+
* Bei value=false wird kein Auto-Reset ausgelöst (z.B. brightness_stop-Signal).
|
|
200
257
|
*
|
|
201
|
-
* @param stateName
|
|
202
|
-
* @param value
|
|
203
|
-
* @param timeout
|
|
258
|
+
* @param {string} stateName Vollständiger State-Pfad
|
|
259
|
+
* @param {boolean} value Der sofort zu setzende Wert
|
|
260
|
+
* @param {number} timeout Millisekunden bis zum Auto-Reset (nur bei value=true)
|
|
204
261
|
*/
|
|
205
262
|
async setStateWithTimeoutAsync(stateName, value, timeout) {
|
|
206
263
|
if (value === undefined || value === null) {
|
|
@@ -208,33 +265,50 @@ class StatesController {
|
|
|
208
265
|
}
|
|
209
266
|
|
|
210
267
|
await this.adapter.setStateAsync(stateName, value, true);
|
|
211
|
-
|
|
212
|
-
|
|
268
|
+
|
|
269
|
+
// Auto-Reset (false → nichts tun, true → nach timeout zurücksetzen)
|
|
270
|
+
// Wenn value=false (z.B. Stop-Aktion im simpleMoveStopState-Modus),
|
|
271
|
+
// soll der State dauerhaft false bleiben und NICHT nach timeout auf true springen.
|
|
272
|
+
if (this.timeOutCache[stateName]) {
|
|
273
|
+
this.adapter.clearTimeout(this.timeOutCache[stateName]);
|
|
274
|
+
delete this.timeOutCache[stateName];
|
|
275
|
+
}
|
|
276
|
+
if (value === true) {
|
|
277
|
+
this.timeOutCache[stateName] = this.adapter.setTimeout(() => {
|
|
278
|
+
delete this.timeOutCache[stateName];
|
|
279
|
+
this.adapter.setStateAsync(stateName, false, true).catch((err) => {
|
|
280
|
+
this.adapter.log.debug(`setStateWithTimeout reset error for ${stateName}: ${err}`);
|
|
281
|
+
});
|
|
282
|
+
}, timeout);
|
|
213
283
|
}
|
|
214
|
-
timeOutCache[stateName] = setTimeout(() => {
|
|
215
|
-
this.adapter.setStateAsync(stateName, !value, true);
|
|
216
|
-
}, timeout);
|
|
217
284
|
}
|
|
218
285
|
|
|
219
286
|
/**
|
|
220
|
-
*
|
|
287
|
+
* Verarbeitet alle in der incStatsQueue gepufferten Nachrichten erneut.
|
|
288
|
+
* Wird nach dem Aufbau des Geräte-/Gruppen-Caches aufgerufen.
|
|
221
289
|
*/
|
|
222
|
-
processQueue() {
|
|
290
|
+
async processQueue() {
|
|
223
291
|
const oldIncStatsQueue = [];
|
|
224
|
-
utils.moveArray(incStatsQueue, oldIncStatsQueue);
|
|
292
|
+
utils.moveArray(this.incStatsQueue, oldIncStatsQueue);
|
|
225
293
|
while (oldIncStatsQueue.length > 0) {
|
|
226
|
-
|
|
294
|
+
// seriell abarbeiten – nicht parallel feuern
|
|
295
|
+
await this.processDeviceMessage(oldIncStatsQueue.shift());
|
|
227
296
|
}
|
|
228
297
|
}
|
|
229
298
|
|
|
230
299
|
/**
|
|
231
|
-
*
|
|
300
|
+
* Meldet alle bisherigen State-Subscriptions ab und subscribt neu
|
|
301
|
+
* nur auf beschreibbare States aller bekannten Geräte und Gruppen.
|
|
232
302
|
*/
|
|
233
|
-
|
|
234
|
-
|
|
303
|
+
subscribeWritableStates() {
|
|
304
|
+
// Alle bestehenden State-Subscriptions zuerst abmelden
|
|
305
|
+
this.adapter.unsubscribeStates('*');
|
|
235
306
|
for (const device of this.groupCache.concat(this.deviceCache)) {
|
|
307
|
+
if (!device || !Array.isArray(device.states)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
236
310
|
for (const state of device.states) {
|
|
237
|
-
if (state.write
|
|
311
|
+
if (state && state.write === true) {
|
|
238
312
|
this.adapter.subscribeStates(`${device.ieee_address}.${state.id}`);
|
|
239
313
|
}
|
|
240
314
|
}
|
|
@@ -245,22 +319,28 @@ class StatesController {
|
|
|
245
319
|
}
|
|
246
320
|
|
|
247
321
|
/**
|
|
248
|
-
*
|
|
322
|
+
* Setzt alle "*.available"-States im Adapter auf false.
|
|
323
|
+
* Wird beim Verbindungsverlust zu Zigbee2MQTT aufgerufen.
|
|
249
324
|
*/
|
|
250
325
|
async setAllAvailableToFalse() {
|
|
251
326
|
const availableStates = await this.adapter.getStatesAsync('*.available');
|
|
252
|
-
|
|
327
|
+
if (!availableStates) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
for (const availableState of Object.keys(availableStates)) {
|
|
253
331
|
await this.adapter.setStateChangedAsync(availableState, false, true);
|
|
254
332
|
}
|
|
255
333
|
}
|
|
256
334
|
|
|
257
335
|
/**
|
|
258
|
-
*
|
|
336
|
+
* Bricht alle laufenden Auto-Reset-Timer ab und leert den Timer-Cache.
|
|
337
|
+
* Wird beim Adapter-Stop aufgerufen.
|
|
259
338
|
*/
|
|
260
|
-
|
|
261
|
-
for (const timer
|
|
262
|
-
clearTimeout(timeOutCache[timer]);
|
|
339
|
+
allTimerClear() {
|
|
340
|
+
for (const timer of Object.keys(this.timeOutCache)) {
|
|
341
|
+
this.adapter.clearTimeout(this.timeOutCache[timer]);
|
|
263
342
|
}
|
|
343
|
+
this.timeOutCache = {};
|
|
264
344
|
}
|
|
265
345
|
}
|
|
266
346
|
|
package/lib/utils.js
CHANGED
|
@@ -1,68 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Konvertiert einen Lampen-Helligkeitswert [0..254] in einen Adapter-Prozentwert [0..100].
|
|
3
3
|
*
|
|
4
|
-
* @param bulbLevel
|
|
4
|
+
* @param {number} bulbLevel Helligkeitswert der Lampe (0–254)
|
|
5
|
+
* @returns {number} Prozentwert (0–100)
|
|
5
6
|
*/
|
|
6
7
|
function bulbLevelToAdapterLevel(bulbLevel) {
|
|
7
|
-
// Convert from bulb levels [0...254] to adapter levels [0...100]:
|
|
8
|
-
// - Bulb level 0 is a forbidden value according to the ZigBee spec "ZigBee Cluster Library
|
|
9
|
-
// (for ZigBee 3.0) User Guide", but some bulbs (HUE) accept this value and interpret this
|
|
10
|
-
// value as "switch the bulb off".
|
|
11
|
-
// - A bulb level of "1" is the "minimum possible level" which should mean "bulb off",
|
|
12
|
-
// but there are bulbs that do not switch off (they need "0", some IKEA bulbs are affected).
|
|
13
|
-
// - No visible difference was seen between bulb level 1 and 2 on HUE LCT012 bulbs.
|
|
14
|
-
//
|
|
15
|
-
// Conclusion:
|
|
16
|
-
// - We map adapter level "0" to the (forbidden) bulb level "0" that seems to switch all
|
|
17
|
-
// known bulbs.
|
|
18
|
-
// - Bulb level "1" is not used, but if received nevertheless, it is converted to
|
|
19
|
-
// adapter level "0" (off).
|
|
20
|
-
// - Bulb level range [2...254] is linearly mapped to adapter level range [1...100].
|
|
21
8
|
if (bulbLevel >= 2) {
|
|
22
9
|
// Perform linear mapping of range [2...254] to [1...100]
|
|
23
10
|
return Math.round(((bulbLevel - 2) * 99) / 252) + 1;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// else
|
|
11
|
+
}
|
|
12
|
+
// The bulb is considered off. Even a bulb level of "1" is considered as off.
|
|
13
|
+
return 0;
|
|
28
14
|
}
|
|
29
15
|
|
|
30
16
|
/**
|
|
31
|
-
*
|
|
17
|
+
* Konvertiert einen Adapter-Prozentwert [0..100] in einen Lampen-Helligkeitswert [0..254].
|
|
32
18
|
*
|
|
33
|
-
* @param adapterLevel
|
|
19
|
+
* @param {number} adapterLevel Prozentwert (0–100)
|
|
20
|
+
* @returns {number} Helligkeitswert der Lampe (0–254)
|
|
34
21
|
*/
|
|
35
22
|
function adapterLevelToBulbLevel(adapterLevel) {
|
|
36
|
-
|
|
37
|
-
// This is the inverse of function bulbLevelToAdapterLevel().
|
|
38
|
-
// Please read the comments there regarding the rules applied here for mapping the values.
|
|
39
|
-
if (adapterLevel) {
|
|
23
|
+
if (adapterLevel != null && adapterLevel > 0) {
|
|
40
24
|
// Perform linear mapping of range [1...100] to [2...254]
|
|
41
25
|
return Math.round(((adapterLevel - 1) * 252) / 99) + 2;
|
|
42
|
-
}
|
|
43
|
-
// Switch the bulb off. Some bulbs need "0" (IKEA), others "1" (HUE), and according to the
|
|
44
|
-
// ZigBee docs "1" is the "minimum possible level"... we choose "0" here which seems to work.
|
|
45
|
-
return 0;
|
|
46
|
-
// else
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
* @param ba
|
|
52
|
-
*/
|
|
53
|
-
function bytesArrayToWordArray(ba) {
|
|
54
|
-
const wa = [];
|
|
55
|
-
for (let i = 0; i < ba.length; i++) {
|
|
56
|
-
wa[(i / 2) | 0] |= ba[i] << (8 * (i % 2));
|
|
57
26
|
}
|
|
58
|
-
|
|
27
|
+
// Switch the bulb off. Some bulbs need "0" (IKEA), others "1" (HUE), and according to the
|
|
28
|
+
// ZigBee docs "1" is the "minimum possible level"... we choose "0" here which seems to work.
|
|
29
|
+
return 0;
|
|
59
30
|
}
|
|
60
31
|
|
|
32
|
+
|
|
61
33
|
// If the value is greater than 1000, kelvin is assumed.
|
|
62
34
|
// If smaller, it is assumed to be mired.
|
|
63
35
|
/**
|
|
36
|
+
* Konvertiert einen Kelvin- oder Mired-Wert immer in Mired.
|
|
37
|
+
* Werte > 1000 werden als Kelvin interpretiert und umgerechnet.
|
|
64
38
|
*
|
|
65
|
-
* @param t
|
|
39
|
+
* @param {number} t Farbtemperatur in Kelvin (>1000) oder Mired (≤1000)
|
|
40
|
+
* @returns {number} Farbtemperatur in Mired
|
|
66
41
|
*/
|
|
67
42
|
function toMired(t) {
|
|
68
43
|
let miredValue = t;
|
|
@@ -73,22 +48,25 @@ function toMired(t) {
|
|
|
73
48
|
}
|
|
74
49
|
|
|
75
50
|
/**
|
|
51
|
+
* Konvertiert zwischen Mired und Kelvin (die Formel ist in beide Richtungen gleich: 1.000.000 / t).
|
|
76
52
|
*
|
|
77
|
-
* @param t
|
|
53
|
+
* @param {number} t Farbtemperatur in Mired oder Kelvin
|
|
54
|
+
* @returns {number} Umgerechneter Wert (gerundet)
|
|
78
55
|
*/
|
|
79
56
|
function miredKelvinConversion(t) {
|
|
80
57
|
return Math.round(1000000 / t);
|
|
81
58
|
}
|
|
82
59
|
|
|
83
60
|
/**
|
|
84
|
-
*
|
|
61
|
+
* Konvertiert eine Dezimalzahl in einen hex-String mit führenden Nullen.
|
|
85
62
|
*
|
|
86
|
-
* @param decimal
|
|
87
|
-
* @param padding
|
|
63
|
+
* @param {number} decimal Die umzuwandelnde Zahl
|
|
64
|
+
* @param {number} [padding] Mindestlänge des hex-Strings (wird mit '0' aufgefüllt)
|
|
65
|
+
* @returns {string} Hex-String in Kleinbuchstaben
|
|
88
66
|
*/
|
|
89
67
|
function decimalToHex(decimal, padding) {
|
|
90
68
|
let hex = Number(decimal).toString(16);
|
|
91
|
-
padding = typeof padding === 'undefined' || padding === null ?
|
|
69
|
+
padding = typeof padding === 'undefined' || padding === null ? 2 : padding;
|
|
92
70
|
|
|
93
71
|
while (hex.length < padding) {
|
|
94
72
|
hex = `0${hex}`;
|
|
@@ -98,81 +76,43 @@ function decimalToHex(decimal, padding) {
|
|
|
98
76
|
}
|
|
99
77
|
|
|
100
78
|
/**
|
|
79
|
+
* Leert ein Array in-place (O(1)).
|
|
101
80
|
*
|
|
102
|
-
* @param
|
|
103
|
-
*/
|
|
104
|
-
function getZbId(adapterDevId) {
|
|
105
|
-
const idx = adapterDevId.indexOf('group');
|
|
106
|
-
if (idx > 0) {
|
|
107
|
-
return adapterDevId.substr(idx + 6);
|
|
108
|
-
}
|
|
109
|
-
return `0x${ adapterDevId.split('.')[2]}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
* @param adapter
|
|
115
|
-
* @param id
|
|
116
|
-
*/
|
|
117
|
-
function getAdId(adapter, id) {
|
|
118
|
-
return `${adapter.namespace }.${ id.split('.')[2]}`; // iobroker device id
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
*
|
|
123
|
-
* @param array
|
|
81
|
+
* @param {Array} array Das zu leerende Array
|
|
124
82
|
*/
|
|
125
83
|
function clearArray(array) {
|
|
126
|
-
|
|
127
|
-
array.pop();
|
|
128
|
-
}
|
|
84
|
+
array.length = 0;
|
|
129
85
|
}
|
|
130
86
|
|
|
131
87
|
/**
|
|
88
|
+
* Verschiebt alle Einträge von source nach target und leert source dabei.
|
|
132
89
|
*
|
|
133
|
-
* @param source
|
|
134
|
-
* @param target
|
|
90
|
+
* @param {Array} source Quell-Array (wird nach dem Aufruf leer sein)
|
|
91
|
+
* @param {Array} target Ziel-Array (bekommt alle Einträge aus source angehängt)
|
|
135
92
|
*/
|
|
136
93
|
function moveArray(source, target) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
94
|
+
target.push(...source);
|
|
95
|
+
source.length = 0;
|
|
140
96
|
}
|
|
141
97
|
|
|
142
98
|
/**
|
|
99
|
+
* Prüft ob ein Wert ein einfaches Objekt ist (kein Array, kein null).
|
|
143
100
|
*
|
|
144
|
-
* @param item
|
|
101
|
+
* @param {*} item Der zu prüfende Wert
|
|
102
|
+
* @returns {boolean} true wenn item ein Objekt ist, sonst false
|
|
145
103
|
*/
|
|
146
104
|
function isObject(item) {
|
|
147
105
|
return typeof item === 'object' && !Array.isArray(item) && item !== null;
|
|
148
106
|
}
|
|
149
107
|
|
|
150
|
-
/**
|
|
151
|
-
*
|
|
152
|
-
* @param item
|
|
153
|
-
*/
|
|
154
|
-
function isJson(item) {
|
|
155
|
-
let value = typeof item !== 'string' ? JSON.stringify(item) : item;
|
|
156
|
-
try {
|
|
157
|
-
value = JSON.parse(value);
|
|
158
|
-
} catch (e) {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return typeof value === 'object' && value !== null;
|
|
163
|
-
}
|
|
164
108
|
|
|
165
109
|
module.exports = {
|
|
166
110
|
bulbLevelToAdapterLevel,
|
|
167
111
|
adapterLevelToBulbLevel,
|
|
168
|
-
bytesArrayToWordArray,
|
|
169
112
|
toMired,
|
|
170
113
|
miredKelvinConversion,
|
|
171
114
|
decimalToHex,
|
|
172
|
-
getZbId,
|
|
173
|
-
getAdId,
|
|
174
115
|
clearArray,
|
|
175
116
|
moveArray,
|
|
176
117
|
isObject,
|
|
177
|
-
isJson,
|
|
178
118
|
};
|