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
|
@@ -1,128 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const WebSocket = require('ws');
|
|
2
|
-
let wsClient;
|
|
3
4
|
const wsHeartbeatIntervall = 5000;
|
|
4
5
|
const restartTimeout = 1000;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
let autoRestartTimeout;
|
|
6
|
+
/** Maximum reconnect delay in milliseconds (caps exponential backoff) */
|
|
7
|
+
const MAX_RESTART_TIMEOUT = 30000;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Verwaltet die WebSocket-Verbindung zu Zigbee2MQTT inklusive
|
|
11
|
+
* Heartbeat-Überwachung und exponentiellem Reconnect-Backoff.
|
|
11
12
|
*/
|
|
12
13
|
class WebsocketController {
|
|
13
14
|
/**
|
|
15
|
+
* Erstellt eine neue WebsocketController-Instanz.
|
|
14
16
|
*
|
|
15
|
-
* @param adapter
|
|
17
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
16
18
|
*/
|
|
17
19
|
constructor(adapter) {
|
|
18
20
|
this.adapter = adapter;
|
|
21
|
+
this.wsClient = null;
|
|
22
|
+
this.ping = null;
|
|
23
|
+
this.pingTimeout = null;
|
|
24
|
+
this.autoRestartTimeout = null;
|
|
25
|
+
// Flag: wird bei closeConnection() gesetzt damit autoRestart() nicht feuert
|
|
26
|
+
this._intentionalClose = false;
|
|
27
|
+
/** Aktueller Reconnect-Delay (exponentielles Backoff) */
|
|
28
|
+
this._reconnectDelay = restartTimeout;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
|
-
*
|
|
32
|
+
* Baut eine neue WebSocket-Verbindung auf.
|
|
33
|
+
* Bestehende Verbindung und alle Timer werden zuerst sicher bereinigt.
|
|
23
34
|
*/
|
|
24
35
|
initWsClient() {
|
|
36
|
+
// Config-Guard: Pflichtfelder müssen vorhanden sein
|
|
37
|
+
if (!this.adapter.config.wsScheme || !this.adapter.config.wsServerIP || !this.adapter.config.wsServerPort) {
|
|
38
|
+
this.adapter.log.error('WebSocket config incomplete (wsScheme / wsServerIP / wsServerPort missing).');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this._intentionalClose = false;
|
|
43
|
+
|
|
44
|
+
// Vorherige Timer stoppen
|
|
45
|
+
this.adapter.clearTimeout(this.ping);
|
|
46
|
+
this.adapter.clearTimeout(this.pingTimeout);
|
|
47
|
+
this.adapter.clearTimeout(this.autoRestartTimeout);
|
|
48
|
+
|
|
49
|
+
// Vorherige Verbindung sicher schließen und null setzen
|
|
50
|
+
if (this.wsClient) {
|
|
51
|
+
this.wsClient.removeAllListeners();
|
|
52
|
+
if (this.wsClient.readyState !== WebSocket.CLOSED &&
|
|
53
|
+
this.wsClient.readyState !== WebSocket.CLOSING) {
|
|
54
|
+
try {
|
|
55
|
+
this.wsClient.terminate();
|
|
56
|
+
} catch (e) {
|
|
57
|
+
this.adapter.log.debug(`initWsClient: old socket terminate error: ${e}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
this.wsClient = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
25
63
|
try {
|
|
26
64
|
let wsURL = `${this.adapter.config.wsScheme}://${this.adapter.config.wsServerIP}:${this.adapter.config.wsServerPort}/api`;
|
|
65
|
+
// Für Logging: URL ohne Token (Sicherheit)
|
|
66
|
+
const wsURLSafe = wsURL;
|
|
27
67
|
|
|
28
|
-
if (this.adapter.config.wsTokenEnabled
|
|
68
|
+
if (this.adapter.config.wsTokenEnabled === true) {
|
|
29
69
|
wsURL += `?token=${this.adapter.config.wsToken}`;
|
|
30
70
|
}
|
|
31
71
|
|
|
32
|
-
|
|
72
|
+
this.adapter.log.debug(`WebSocket connecting to ${wsURLSafe}`);
|
|
73
|
+
this.wsClient = new WebSocket(wsURL, { rejectUnauthorized: false });
|
|
33
74
|
|
|
34
|
-
wsClient.on('open', () => {
|
|
35
|
-
//
|
|
75
|
+
this.wsClient.on('open', () => {
|
|
76
|
+
this._reconnectDelay = restartTimeout; // Backoff zurücksetzen bei Erfolg
|
|
77
|
+
this.adapter.log.info('Connect to Zigbee2MQTT over websocket connection.');
|
|
36
78
|
this.sendPingToServer();
|
|
37
|
-
// Start Heartbeat
|
|
38
79
|
this.wsHeartbeat();
|
|
39
80
|
});
|
|
40
81
|
|
|
41
|
-
wsClient.on('pong', () => {
|
|
82
|
+
this.wsClient.on('pong', () => {
|
|
42
83
|
this.wsHeartbeat();
|
|
43
84
|
});
|
|
44
85
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
// ws@8 liefert Buffer, nicht String → immer .toString() verwenden
|
|
87
|
+
this.wsClient.on('message', (message) => {
|
|
88
|
+
let messageObj;
|
|
89
|
+
try {
|
|
90
|
+
messageObj = JSON.parse(message.toString());
|
|
91
|
+
} catch {
|
|
92
|
+
this.adapter.log.debug(`Invalid WebSocket message: ${message.toString().slice(0, 200)}`);
|
|
93
|
+
return;
|
|
51
94
|
}
|
|
95
|
+
// messageParse ist async – Fehler mit .catch() abfangen
|
|
96
|
+
this.adapter.messageParse(messageObj).catch((err) => {
|
|
97
|
+
this.adapter.log.error(`messageParse error: ${err}`);
|
|
98
|
+
});
|
|
52
99
|
});
|
|
53
100
|
|
|
54
|
-
wsClient.on('
|
|
101
|
+
this.wsClient.on('close', (code, reason) => {
|
|
102
|
+
this._onWsClose(code, reason).catch((err) => {
|
|
103
|
+
this.adapter.log.error(`WebSocket close handler error: ${err}`);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
55
106
|
|
|
56
|
-
wsClient.on('error', (err) => {
|
|
57
|
-
|
|
107
|
+
this.wsClient.on('error', (err) => {
|
|
108
|
+
// err kann undefined sein oder kein Error-Objekt
|
|
109
|
+
const msg = err && err.message ? err.message : String(err || 'unknown');
|
|
110
|
+
this.adapter.log.debug(`WebSocket error: ${msg}`);
|
|
111
|
+
// Kein throw – der 'close'-Event folgt nach 'error' immer automatisch
|
|
58
112
|
});
|
|
59
113
|
|
|
60
|
-
return wsClient;
|
|
61
114
|
} catch (err) {
|
|
62
|
-
this.adapter.log.error(err);
|
|
115
|
+
this.adapter.log.error(`WebSocket init error: ${err}`);
|
|
116
|
+
// Retry nach restartTimeout
|
|
117
|
+
if (!this._intentionalClose) {
|
|
118
|
+
this.autoRestart();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Interner async Handler für den WebSocket-close-Event.
|
|
125
|
+
*
|
|
126
|
+
* @param {number} code WebSocket-Close-Code (z.B. 1000 = normal, 1006 = abnormal)
|
|
127
|
+
* @param {Buffer} reason Optionaler Grund-Text des Close-Events
|
|
128
|
+
*/
|
|
129
|
+
async _onWsClose(code, reason) {
|
|
130
|
+
this.adapter.clearTimeout(this.pingTimeout);
|
|
131
|
+
this.adapter.clearTimeout(this.ping);
|
|
132
|
+
|
|
133
|
+
this.adapter.log.debug(`WebSocket closed – code: ${code}, reason: ${reason ? reason.toString() : 'none'}`);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (this.adapter.statesController) {
|
|
137
|
+
this.adapter.setStateChanged('info.connection', false, true);
|
|
138
|
+
await this.adapter.statesController.setAllAvailableToFalse();
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
this.adapter.log.error(`close handler setAllAvailableToFalse error: ${err}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Caches leeren
|
|
145
|
+
this.adapter.deviceCache.length = 0;
|
|
146
|
+
this.adapter.groupCache.length = 0;
|
|
147
|
+
for (const key of Object.keys(this.adapter.createCache)) {
|
|
148
|
+
delete this.adapter.createCache[key];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Nur reconnecten wenn kein intentionales Shutdown
|
|
152
|
+
if (!this._intentionalClose) {
|
|
153
|
+
this.autoRestart();
|
|
63
154
|
}
|
|
64
155
|
}
|
|
65
156
|
|
|
66
157
|
/**
|
|
158
|
+
* Sendet eine serialisierte Nachricht an Zigbee2MQTT über den WebSocket.
|
|
67
159
|
*
|
|
68
|
-
* @param message
|
|
160
|
+
* @param {string} message JSON-String der zu sendenden Nachricht
|
|
69
161
|
*/
|
|
70
162
|
send(message) {
|
|
71
|
-
if (wsClient.readyState !== WebSocket.OPEN) {
|
|
163
|
+
if (!this.wsClient || this.wsClient.readyState !== WebSocket.OPEN) {
|
|
72
164
|
this.adapter.log.warn('Cannot set State, no websocket connection to Zigbee2MQTT!');
|
|
73
165
|
return;
|
|
74
166
|
}
|
|
75
|
-
|
|
167
|
+
try {
|
|
168
|
+
this.wsClient.send(message);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
this.adapter.log.error(`WebSocket send error: ${err && err.message ? err.message : String(err)}`);
|
|
171
|
+
}
|
|
76
172
|
}
|
|
77
173
|
|
|
78
174
|
/**
|
|
79
|
-
*
|
|
175
|
+
* Sendet regelmäßig WebSocket-Pings an den Z2M-Server
|
|
176
|
+
* und plant den nächsten Ping nach wsHeartbeatIntervall Millisekunden.
|
|
80
177
|
*/
|
|
81
178
|
sendPingToServer() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
179
|
+
if (!this.wsClient || this.wsClient.readyState !== WebSocket.OPEN) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
this.wsClient.ping();
|
|
184
|
+
} catch (err) {
|
|
185
|
+
this.adapter.log.debug(`WebSocket ping error: ${err && err.message ? err.message : String(err)}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.ping = this.adapter.setTimeout(() => {
|
|
85
189
|
this.sendPingToServer();
|
|
86
190
|
}, wsHeartbeatIntervall);
|
|
87
191
|
}
|
|
88
192
|
|
|
89
193
|
/**
|
|
90
|
-
*
|
|
194
|
+
* Startet oder erneuert den Heartbeat-Timeout.
|
|
195
|
+
* Wenn innerhalb von wsHeartbeatIntervall + 3000 ms kein Pong eintrifft,
|
|
196
|
+
* wird die Verbindung terminiert und autoRestart() ausgelöst.
|
|
91
197
|
*/
|
|
92
198
|
wsHeartbeat() {
|
|
93
|
-
clearTimeout(pingTimeout);
|
|
94
|
-
pingTimeout = setTimeout(() => {
|
|
95
|
-
this.adapter.log.warn('
|
|
96
|
-
|
|
199
|
+
this.adapter.clearTimeout(this.pingTimeout);
|
|
200
|
+
this.pingTimeout = this.adapter.setTimeout(() => {
|
|
201
|
+
this.adapter.log.warn('WebSocket connection timed out – terminating.');
|
|
202
|
+
try {
|
|
203
|
+
if (this.wsClient) {
|
|
204
|
+
this.wsClient.terminate();
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
this.adapter.log.debug(`wsHeartbeat terminate error: ${err}`);
|
|
208
|
+
}
|
|
209
|
+
// terminate() feuert 'close' → autoRestart() wird dort aufgerufen
|
|
97
210
|
}, wsHeartbeatIntervall + 3000);
|
|
98
211
|
}
|
|
99
212
|
|
|
100
213
|
/**
|
|
101
|
-
*
|
|
214
|
+
* Plant einen Reconnect-Versuch mit exponentiellem Backoff.
|
|
215
|
+
* Der Delay verdoppelt sich mit jedem Versuch bis maximal MAX_RESTART_TIMEOUT ms.
|
|
102
216
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
217
|
+
autoRestart() {
|
|
218
|
+
const delay = this._reconnectDelay;
|
|
219
|
+
this._reconnectDelay = Math.min(this._reconnectDelay * 2, MAX_RESTART_TIMEOUT);
|
|
220
|
+
this.adapter.log.warn(`WebSocket disconnected – reconnecting in ${delay / 1000} second(s)...`);
|
|
221
|
+
this.adapter.clearTimeout(this.autoRestartTimeout);
|
|
222
|
+
this.autoRestartTimeout = this.adapter.setTimeout(() => {
|
|
223
|
+
try {
|
|
224
|
+
this.initWsClient();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
this.adapter.log.error(`autoRestart initWsClient error: ${err}`);
|
|
227
|
+
}
|
|
228
|
+
}, delay);
|
|
108
229
|
}
|
|
109
230
|
|
|
110
231
|
/**
|
|
111
|
-
*
|
|
232
|
+
* Schließt die WebSocket-Verbindung intentional (kein autoRestart).
|
|
233
|
+
* Wird beim Adapter-Stop aufgerufen.
|
|
112
234
|
*/
|
|
113
235
|
closeConnection() {
|
|
114
|
-
|
|
115
|
-
|
|
236
|
+
this._intentionalClose = true;
|
|
237
|
+
this._reconnectDelay = restartTimeout; // Backoff zurücksetzen
|
|
238
|
+
this.adapter.clearTimeout(this.ping);
|
|
239
|
+
this.adapter.clearTimeout(this.pingTimeout);
|
|
240
|
+
this.adapter.clearTimeout(this.autoRestartTimeout);
|
|
241
|
+
if (this.wsClient) {
|
|
242
|
+
this.wsClient.removeAllListeners();
|
|
243
|
+
try {
|
|
244
|
+
if (this.wsClient.readyState !== WebSocket.CLOSED &&
|
|
245
|
+
this.wsClient.readyState !== WebSocket.CLOSING) {
|
|
246
|
+
this.wsClient.close();
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
this.adapter.log.debug(`closeConnection error: ${err}`);
|
|
250
|
+
}
|
|
251
|
+
this.wsClient = null;
|
|
116
252
|
}
|
|
117
253
|
}
|
|
118
254
|
|
|
119
255
|
/**
|
|
120
|
-
*
|
|
256
|
+
* Stoppt alle laufenden Timer (ping, pingTimeout, autoRestartTimeout).
|
|
257
|
+
* Wird beim Adapter-Stop aufgerufen.
|
|
121
258
|
*/
|
|
122
|
-
|
|
123
|
-
clearTimeout(pingTimeout);
|
|
124
|
-
clearTimeout(ping);
|
|
125
|
-
clearTimeout(autoRestartTimeout);
|
|
259
|
+
allTimerClear() {
|
|
260
|
+
this.adapter.clearTimeout(this.pingTimeout);
|
|
261
|
+
this.adapter.clearTimeout(this.ping);
|
|
262
|
+
this.adapter.clearTimeout(this.autoRestartTimeout);
|
|
126
263
|
}
|
|
127
264
|
}
|
|
128
265
|
|
package/lib/z2mController.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Übersetzt ioBroker-State-Änderungen in Zigbee2MQTT-MQTT-Nachrichten
|
|
3
|
+
* und leitet Z2M-Log-Nachrichten in den ioBroker-Logger weiter.
|
|
3
4
|
*/
|
|
4
5
|
class Z2mController {
|
|
5
6
|
/**
|
|
7
|
+
* Erstellt eine neue Z2mController-Instanz.
|
|
6
8
|
*
|
|
7
|
-
* @param adapter
|
|
8
|
-
* @param deviceCache
|
|
9
|
-
* @param groupCache
|
|
10
|
-
* @param logCustomizations
|
|
9
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
10
|
+
* @param {Array} deviceCache Gemeinsamer Cache aller bekannten Geräte
|
|
11
|
+
* @param {Array} groupCache Gemeinsamer Cache aller bekannten Gruppen
|
|
12
|
+
* @param {object} logCustomizations Debug/Filter-Einstellungen (debugDevices, logfilter)
|
|
11
13
|
*/
|
|
12
14
|
constructor(adapter, deviceCache, groupCache, logCustomizations) {
|
|
13
15
|
this.adapter = adapter;
|
|
@@ -17,9 +19,12 @@ class Z2mController {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
22
|
+
* Erzeugt aus einer ioBroker-State-Änderung eine Zigbee2MQTT-Nachricht.
|
|
23
|
+
* Gibt null zurück wenn kein Gerät/State gefunden wurde oder der Setter undefined liefert.
|
|
20
24
|
*
|
|
21
|
-
* @param id
|
|
22
|
-
* @param state
|
|
25
|
+
* @param {string} id Vollständige State-ID (z.B. "zigbee2mqtt.0.0xAABB.state")
|
|
26
|
+
* @param {ioBroker.State} state Das neue State-Objekt mit val und ack
|
|
27
|
+
* @returns {Promise<{topic: string, payload: object}|undefined>} Die Z2M-Nachricht oder undefined
|
|
23
28
|
*/
|
|
24
29
|
async createZ2MMessage(id, state) {
|
|
25
30
|
const splitedID = id.split('.');
|
|
@@ -29,25 +34,38 @@ class Z2mController {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
if (id.endsWith('info.coordinator_check')) {
|
|
32
|
-
|
|
37
|
+
// Fix 1: Z2M erwartet JSON-Objekt {} nicht leeren String ''
|
|
38
|
+
return { topic: 'bridge/request/coordinator_check', payload: {} };
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const ieee_address = splitedID[2];
|
|
36
42
|
const stateName = splitedID[3];
|
|
37
43
|
|
|
38
|
-
const device = this.groupCache.concat(this.deviceCache).find((d) => d.ieee_address
|
|
44
|
+
const device = this.groupCache.concat(this.deviceCache).find((d) => d.ieee_address === ieee_address);
|
|
39
45
|
if (!device) {
|
|
40
46
|
return;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
const deviceState = device.states.find((s) => s.id
|
|
49
|
+
const deviceState = device.states.find((s) => s.id === stateName);
|
|
44
50
|
if (!deviceState) {
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
let stateVal = state.val;
|
|
49
55
|
if (deviceState.setter) {
|
|
50
|
-
|
|
56
|
+
// Fix 3: setter kann crashen (z.B. ungültiger Farbwert) → try/catch
|
|
57
|
+
try {
|
|
58
|
+
stateVal = deviceState.setter(state.val);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
this.adapter.log.warn(`${device.ieee_address} state: ${stateName} setter error: ${err.message || err}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fix 5: wenn setter undefined zurückgibt (z.B. Toggle-Release) → nichts senden
|
|
66
|
+
if (stateVal === undefined) {
|
|
67
|
+
this.adapter.log.debug(`${device.ieee_address} state: ${stateName} setter returned undefined, skipping`);
|
|
68
|
+
return;
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
let stateID = deviceState.id;
|
|
@@ -66,16 +84,18 @@ class Z2mController {
|
|
|
66
84
|
topic: `${device.id}/set`,
|
|
67
85
|
};
|
|
68
86
|
|
|
69
|
-
if (stateID
|
|
87
|
+
if (stateID === 'send_payload') {
|
|
70
88
|
try {
|
|
71
89
|
controlObj.payload = JSON.parse(stateVal);
|
|
72
|
-
this.adapter.
|
|
90
|
+
await this.adapter.setStateAsync(id, state.val, true);
|
|
73
91
|
} catch (error) {
|
|
74
|
-
|
|
92
|
+
// Fix 9: rawStr als String direkt als payload – wird in main.js via
|
|
93
|
+
// JSON.stringify() nochmals gewrappt → würde doppelt quoten.
|
|
94
|
+
// Stattdessen: Fehler loggen und NICHT senden (ungültiges JSON bleibt ungültig)
|
|
75
95
|
this.adapter.log.warn(
|
|
76
|
-
`${device.ieee_address} state: ${stateID} error: value passed is not a valid JSON`
|
|
96
|
+
`${device.ieee_address} state: ${stateID} error: value passed is not a valid JSON – not sent`
|
|
77
97
|
);
|
|
78
|
-
this.adapter.log.debug(`${device.ieee_address}
|
|
98
|
+
this.adapter.log.debug(`${device.ieee_address} raw value: ${stateVal}`);
|
|
79
99
|
return;
|
|
80
100
|
}
|
|
81
101
|
}
|
|
@@ -83,17 +103,13 @@ class Z2mController {
|
|
|
83
103
|
// if available read option and set payload
|
|
84
104
|
if (deviceState.options) {
|
|
85
105
|
for (const option of deviceState.options) {
|
|
86
|
-
//
|
|
87
|
-
if (!device.optionsValues
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
).val;
|
|
91
|
-
// optionsValues Cache
|
|
92
|
-
device.optionsValues[option] = optionValue;
|
|
106
|
+
// Fix: "in"-Prüfung statt === undefined, damit null als gültiger gecachter Wert behandelt wird
|
|
107
|
+
if (!(option in device.optionsValues)) {
|
|
108
|
+
const optState = await this.adapter.getStateAsync(`${splitedID[0]}.${splitedID[1]}.${splitedID[2]}.${option}`);
|
|
109
|
+
device.optionsValues[option] = optState ? optState.val : null;
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
if (option == 'transition' && device.optionsValues[option] == -1) {
|
|
112
|
+
if (option === 'transition' && device.optionsValues[option] === -1) {
|
|
97
113
|
continue;
|
|
98
114
|
}
|
|
99
115
|
|
|
@@ -103,38 +119,40 @@ class Z2mController {
|
|
|
103
119
|
|
|
104
120
|
// If an option datapoint has been set, it does not have to be sent.
|
|
105
121
|
// This is confirmed directly by the adapter (ack = true)
|
|
106
|
-
if (deviceState.isOption
|
|
122
|
+
if (deviceState.isOption) {
|
|
107
123
|
// set optionsValues 'Cache'
|
|
108
124
|
device.optionsValues[stateName] = state.val;
|
|
109
|
-
this.adapter.
|
|
125
|
+
await this.adapter.setStateAsync(id, state.val, true);
|
|
110
126
|
return;
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
['brightness_move', 'colortemp_move', 'brightness_move', 'brightness_step', 'effect'].includes(
|
|
120
|
-
deviceState.id
|
|
121
|
-
)
|
|
122
|
-
) {
|
|
123
|
-
this.adapter.setState(id, state, true);
|
|
129
|
+
// States die nicht von Z2M zurückgemeldet werden → sofort ack=true setzen
|
|
130
|
+
const immediateAckRole = ['button'].includes(deviceState.role);
|
|
131
|
+
const immediateAckId = ['brightness_move', 'colortemp_move', 'brightness_step', 'effect'].includes(deviceState.id);
|
|
132
|
+
if (immediateAckRole || immediateAckId) {
|
|
133
|
+
await this.adapter.setStateAsync(id, state.val, true);
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
if (this.logCustomizations.debugDevices
|
|
127
|
-
this.
|
|
136
|
+
if (this.logCustomizations.debugDevices) {
|
|
137
|
+
const debugList = String(this.logCustomizations.debugDevices).split(',').map((s) => s.trim());
|
|
138
|
+
if (debugList.includes(device.ieee_address)) {
|
|
139
|
+
this.adapter.log.warn(`<<<--- toZ2M -> ${device.ieee_address} states: ${JSON.stringify(controlObj)}`);
|
|
140
|
+
}
|
|
128
141
|
}
|
|
129
142
|
return controlObj;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
/**
|
|
146
|
+
* Leitet eine Z2M-Log-Nachricht (bridge/logging) an den ioBroker-Logger weiter.
|
|
147
|
+
* Nachrichten die dem konfigurierten Logfilter entsprechen werden unterdrückt.
|
|
133
148
|
*
|
|
134
|
-
* @param messageObj
|
|
149
|
+
* @param {{ payload: { message: string, level: string } }} messageObj Die Log-Nachricht
|
|
135
150
|
*/
|
|
136
151
|
async proxyZ2MLogs(messageObj) {
|
|
137
|
-
const logMessage = messageObj.payload.message;
|
|
152
|
+
const logMessage = messageObj.payload && messageObj.payload.message;
|
|
153
|
+
if (!logMessage) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
138
156
|
if (this.logCustomizations.logfilter.some((x) => logMessage.includes(x))) {
|
|
139
157
|
return;
|
|
140
158
|
}
|
|
@@ -149,6 +167,9 @@ class Z2mController {
|
|
|
149
167
|
case 'warning':
|
|
150
168
|
this.adapter.log.warn(logMessage);
|
|
151
169
|
break;
|
|
170
|
+
default:
|
|
171
|
+
this.adapter.log.debug(`Z2M [${logLevel}]: ${logMessage}`);
|
|
172
|
+
break;
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
175
|
}
|