iobroker.zigbee2mqtt 3.0.21 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/utils.js CHANGED
@@ -46,17 +46,6 @@ function adapterLevelToBulbLevel(adapterLevel) {
46
46
  // else
47
47
  }
48
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
- }
58
- return wa;
59
- }
60
49
 
61
50
  // If the value is greater than 1000, kelvin is assumed.
62
51
  // If smaller, it is assumed to be mired.
@@ -88,7 +77,7 @@ function miredKelvinConversion(t) {
88
77
  */
89
78
  function decimalToHex(decimal, padding) {
90
79
  let hex = Number(decimal).toString(16);
91
- padding = typeof padding === 'undefined' || padding === null ? (padding = 2) : padding;
80
+ padding = typeof padding === 'undefined' || padding === null ? 2 : padding;
92
81
 
93
82
  while (hex.length < padding) {
94
83
  hex = `0${hex}`;
@@ -97,26 +86,6 @@ function decimalToHex(decimal, padding) {
97
86
  return hex;
98
87
  }
99
88
 
100
- /**
101
- *
102
- * @param adapterDevId
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
89
 
121
90
  /**
122
91
  *
@@ -147,32 +116,14 @@ function isObject(item) {
147
116
  return typeof item === 'object' && !Array.isArray(item) && item !== null;
148
117
  }
149
118
 
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
119
 
165
120
  module.exports = {
166
121
  bulbLevelToAdapterLevel,
167
122
  adapterLevelToBulbLevel,
168
- bytesArrayToWordArray,
169
123
  toMired,
170
124
  miredKelvinConversion,
171
125
  decimalToHex,
172
- getZbId,
173
- getAdId,
174
126
  clearArray,
175
127
  moveArray,
176
128
  isObject,
177
- isJson,
178
129
  };
@@ -1,128 +1,233 @@
1
+ 'use strict';
2
+
1
3
  const WebSocket = require('ws');
2
- let wsClient;
3
4
  const wsHeartbeatIntervall = 5000;
4
5
  const restartTimeout = 1000;
5
- let ping;
6
- let pingTimeout;
7
- let autoRestartTimeout;
8
6
 
9
7
  /**
10
8
  *
11
9
  */
12
10
  class WebsocketController {
13
11
  /**
14
- *
15
12
  * @param adapter
16
13
  */
17
14
  constructor(adapter) {
18
15
  this.adapter = adapter;
16
+ this.wsClient = null;
17
+ this.ping = null;
18
+ this.pingTimeout = null;
19
+ this.autoRestartTimeout = null;
20
+ // Flag: wird bei closeConnection() gesetzt damit autoRestart() nicht feuert
21
+ this._intentionalClose = false;
19
22
  }
20
23
 
21
24
  /**
22
- *
25
+ * Baut eine neue WebSocket-Verbindung auf.
26
+ * Bestehende Verbindung und alle Timer werden zuerst sicher bereinigt.
23
27
  */
24
28
  initWsClient() {
29
+ // Config-Guard: Pflichtfelder müssen vorhanden sein
30
+ if (!this.adapter.config.wsScheme || !this.adapter.config.wsServerIP || !this.adapter.config.wsServerPort) {
31
+ this.adapter.log.error('WebSocket config incomplete (wsScheme / wsServerIP / wsServerPort missing).');
32
+ return;
33
+ }
34
+
35
+ this._intentionalClose = false;
36
+
37
+ // Vorherige Timer stoppen
38
+ clearTimeout(this.ping);
39
+ clearTimeout(this.pingTimeout);
40
+ clearTimeout(this.autoRestartTimeout);
41
+
42
+ // Vorherige Verbindung sicher schließen und null setzen
43
+ if (this.wsClient) {
44
+ this.wsClient.removeAllListeners();
45
+ if (this.wsClient.readyState !== WebSocket.CLOSED &&
46
+ this.wsClient.readyState !== WebSocket.CLOSING) {
47
+ try {
48
+ this.wsClient.terminate();
49
+ } catch (e) {
50
+ this.adapter.log.debug(`initWsClient: old socket terminate error: ${e}`);
51
+ }
52
+ }
53
+ this.wsClient = null;
54
+ }
55
+
25
56
  try {
26
57
  let wsURL = `${this.adapter.config.wsScheme}://${this.adapter.config.wsServerIP}:${this.adapter.config.wsServerPort}/api`;
27
58
 
28
- if (this.adapter.config.wsTokenEnabled == true) {
59
+ if (this.adapter.config.wsTokenEnabled === true) {
29
60
  wsURL += `?token=${this.adapter.config.wsToken}`;
30
61
  }
31
62
 
32
- wsClient = new WebSocket(wsURL, { rejectUnauthorized: false });
63
+ this.wsClient = new WebSocket(wsURL, { rejectUnauthorized: false });
33
64
 
34
- wsClient.on('open', () => {
35
- // Send ping to server
65
+ this.wsClient.on('open', () => {
66
+ this.adapter.log.info('Connect to Zigbee2MQTT over websocket connection.');
36
67
  this.sendPingToServer();
37
- // Start Heartbeat
38
68
  this.wsHeartbeat();
39
69
  });
40
70
 
41
- wsClient.on('pong', () => {
71
+ this.wsClient.on('pong', () => {
42
72
  this.wsHeartbeat();
43
73
  });
44
74
 
45
- wsClient.on('close', async () => {
46
- clearTimeout(pingTimeout);
47
- clearTimeout(ping);
75
+ // ws@8 liefert Buffer, nicht String → immer .toString() verwenden
76
+ this.wsClient.on('message', (message) => {
77
+ let messageObj;
78
+ try {
79
+ messageObj = JSON.parse(message.toString());
80
+ } catch {
81
+ this.adapter.log.debug(`Invalid WebSocket message: ${message.toString().slice(0, 200)}`);
82
+ return;
83
+ }
84
+ // messageParse ist async – Fehler mit .catch() abfangen
85
+ this.adapter.messageParse(messageObj).catch((err) => {
86
+ this.adapter.log.error(`messageParse error: ${err}`);
87
+ });
88
+ });
89
+
90
+ this.wsClient.on('close', async (code, reason) => {
91
+ clearTimeout(this.pingTimeout);
92
+ clearTimeout(this.ping);
93
+
94
+ this.adapter.log.debug(`WebSocket closed – code: ${code}, reason: ${reason ? reason.toString() : 'none'}`);
95
+
96
+ try {
97
+ if (this.adapter.statesController) {
98
+ this.adapter.setStateChanged('info.connection', false, true);
99
+ await this.adapter.statesController.setAllAvailableToFalse();
100
+ }
101
+ } catch (err) {
102
+ this.adapter.log.error(`close handler setAllAvailableToFalse error: ${err}`);
103
+ }
104
+
105
+ // Caches leeren
106
+ this.adapter.deviceCache.length = 0;
107
+ this.adapter.groupCache.length = 0;
108
+ for (const key of Object.keys(this.adapter.createCache)) {
109
+ delete this.adapter.createCache[key];
110
+ }
48
111
 
49
- if (wsClient.readyState === WebSocket.CLOSED) {
112
+ // Nur reconnecten wenn kein intentionales Shutdown
113
+ if (!this._intentionalClose) {
50
114
  this.autoRestart();
51
115
  }
52
116
  });
53
117
 
54
- wsClient.on('message', () => {});
55
-
56
- wsClient.on('error', (err) => {
57
- this.adapter.log.debug(err);
118
+ this.wsClient.on('error', (err) => {
119
+ // err kann undefined sein oder kein Error-Objekt
120
+ const msg = err && err.message ? err.message : String(err || 'unknown');
121
+ this.adapter.log.debug(`WebSocket error: ${msg}`);
122
+ // Kein throw – der 'close'-Event folgt nach 'error' immer automatisch
58
123
  });
59
124
 
60
- return wsClient;
61
125
  } catch (err) {
62
- this.adapter.log.error(err);
126
+ this.adapter.log.error(`WebSocket init error: ${err}`);
127
+ // Retry nach restartTimeout
128
+ if (!this._intentionalClose) {
129
+ this.autoRestart();
130
+ }
63
131
  }
64
132
  }
65
133
 
66
134
  /**
135
+ * Sendet eine Nachricht an Z2M.
67
136
  *
68
- * @param message
137
+ * @param {string} message
69
138
  */
70
139
  send(message) {
71
- if (wsClient.readyState !== WebSocket.OPEN) {
140
+ if (!this.wsClient || this.wsClient.readyState !== WebSocket.OPEN) {
72
141
  this.adapter.log.warn('Cannot set State, no websocket connection to Zigbee2MQTT!');
73
142
  return;
74
143
  }
75
- wsClient.send(message);
144
+ try {
145
+ this.wsClient.send(message);
146
+ } catch (err) {
147
+ this.adapter.log.error(`WebSocket send error: ${err && err.message ? err.message : String(err)}`);
148
+ }
76
149
  }
77
150
 
78
151
  /**
79
- *
152
+ * Sendet regelmäßig Pings an den Server.
80
153
  */
81
154
  sendPingToServer() {
82
- //this.logDebug('Send ping to server');
83
- wsClient.ping();
84
- ping = setTimeout(() => {
155
+ if (!this.wsClient || this.wsClient.readyState !== WebSocket.OPEN) {
156
+ return;
157
+ }
158
+ try {
159
+ this.wsClient.ping();
160
+ } catch (err) {
161
+ this.adapter.log.debug(`WebSocket ping error: ${err && err.message ? err.message : String(err)}`);
162
+ return;
163
+ }
164
+ this.ping = setTimeout(() => {
85
165
  this.sendPingToServer();
86
166
  }, wsHeartbeatIntervall);
87
167
  }
88
168
 
89
169
  /**
90
- *
170
+ * Überwacht den Heartbeat. Wenn kein Pong innerhalb des Timeouts kommt, wird die Verbindung getrennt.
91
171
  */
92
172
  wsHeartbeat() {
93
- clearTimeout(pingTimeout);
94
- pingTimeout = setTimeout(() => {
95
- this.adapter.log.warn('Websocked connection timed out');
96
- wsClient.terminate();
173
+ clearTimeout(this.pingTimeout);
174
+ this.pingTimeout = setTimeout(() => {
175
+ this.adapter.log.warn('WebSocket connection timed out – terminating.');
176
+ try {
177
+ if (this.wsClient) {
178
+ this.wsClient.terminate();
179
+ }
180
+ } catch (err) {
181
+ this.adapter.log.debug(`wsHeartbeat terminate error: ${err}`);
182
+ }
183
+ // terminate() feuert 'close' → autoRestart() wird dort aufgerufen
97
184
  }, wsHeartbeatIntervall + 3000);
98
185
  }
99
186
 
100
187
  /**
101
- *
188
+ * Wartet kurz und baut dann die Verbindung neu auf.
102
189
  */
103
- async autoRestart() {
104
- this.adapter.log.warn(`Start try again in ${restartTimeout / 1000} seconds...`);
105
- autoRestartTimeout = setTimeout(() => {
106
- this.adapter.startWebsocket();
190
+ autoRestart() {
191
+ this.adapter.log.warn(`WebSocket disconnected reconnecting in ${restartTimeout / 1000} second(s)...`);
192
+ clearTimeout(this.autoRestartTimeout);
193
+ this.autoRestartTimeout = setTimeout(() => {
194
+ try {
195
+ this.initWsClient();
196
+ } catch (err) {
197
+ this.adapter.log.error(`autoRestart initWsClient error: ${err}`);
198
+ }
107
199
  }, restartTimeout);
108
200
  }
109
201
 
110
202
  /**
111
- *
203
+ * Schließt die Verbindung intentional (kein autoRestart).
112
204
  */
113
205
  closeConnection() {
114
- if (wsClient && wsClient.readyState !== WebSocket.CLOSED) {
115
- wsClient.close();
206
+ this._intentionalClose = true;
207
+ clearTimeout(this.ping);
208
+ clearTimeout(this.pingTimeout);
209
+ clearTimeout(this.autoRestartTimeout);
210
+ if (this.wsClient) {
211
+ this.wsClient.removeAllListeners();
212
+ try {
213
+ if (this.wsClient.readyState !== WebSocket.CLOSED &&
214
+ this.wsClient.readyState !== WebSocket.CLOSING) {
215
+ this.wsClient.close();
216
+ }
217
+ } catch (err) {
218
+ this.adapter.log.debug(`closeConnection error: ${err}`);
219
+ }
220
+ this.wsClient = null;
116
221
  }
117
222
  }
118
223
 
119
224
  /**
120
- *
225
+ * Stoppt alle Timer (z.B. beim Adapter-Stop).
121
226
  */
122
227
  async allTimerClear() {
123
- clearTimeout(pingTimeout);
124
- clearTimeout(ping);
125
- clearTimeout(autoRestartTimeout);
228
+ clearTimeout(this.pingTimeout);
229
+ clearTimeout(this.ping);
230
+ clearTimeout(this.autoRestartTimeout);
126
231
  }
127
232
  }
128
233
 
@@ -29,25 +29,38 @@ class Z2mController {
29
29
  }
30
30
 
31
31
  if (id.endsWith('info.coordinator_check')) {
32
- return { topic: 'bridge/request/coordinator_check', payload: '' };
32
+ // Fix 1: Z2M erwartet JSON-Objekt {} nicht leeren String ''
33
+ return { topic: 'bridge/request/coordinator_check', payload: {} };
33
34
  }
34
35
 
35
36
  const ieee_address = splitedID[2];
36
37
  const stateName = splitedID[3];
37
38
 
38
- const device = this.groupCache.concat(this.deviceCache).find((d) => d.ieee_address == ieee_address);
39
+ const device = this.groupCache.concat(this.deviceCache).find((d) => d.ieee_address === ieee_address);
39
40
  if (!device) {
40
41
  return;
41
42
  }
42
43
 
43
- const deviceState = device.states.find((s) => s.id == stateName);
44
+ const deviceState = device.states.find((s) => s.id === stateName);
44
45
  if (!deviceState) {
45
46
  return;
46
47
  }
47
48
 
48
49
  let stateVal = state.val;
49
50
  if (deviceState.setter) {
50
- stateVal = deviceState.setter(state.val);
51
+ // Fix 3: setter kann crashen (z.B. ungültiger Farbwert) → try/catch
52
+ try {
53
+ stateVal = deviceState.setter(state.val);
54
+ } catch (err) {
55
+ this.adapter.log.warn(`${device.ieee_address} state: ${stateName} setter error: ${err.message || err}`);
56
+ return;
57
+ }
58
+ }
59
+
60
+ // Fix 5: wenn setter undefined zurückgibt (z.B. Toggle-Release) → nichts senden
61
+ if (stateVal === undefined) {
62
+ this.adapter.log.debug(`${device.ieee_address} state: ${stateName} setter returned undefined, skipping`);
63
+ return;
51
64
  }
52
65
 
53
66
  let stateID = deviceState.id;
@@ -66,16 +79,18 @@ class Z2mController {
66
79
  topic: `${device.id}/set`,
67
80
  };
68
81
 
69
- if (stateID == 'send_payload') {
82
+ if (stateID === 'send_payload') {
70
83
  try {
71
84
  controlObj.payload = JSON.parse(stateVal);
72
85
  this.adapter.setState(id, state, true);
73
86
  } catch (error) {
74
- controlObj.payload = stateVal.replaceAll(' ', '').replaceAll('\n', '');
87
+ // Fix 9: rawStr als String direkt als payload wird in main.js via
88
+ // JSON.stringify() nochmals gewrappt → würde doppelt quoten.
89
+ // Stattdessen: Fehler loggen und NICHT senden (ungültiges JSON bleibt ungültig)
75
90
  this.adapter.log.warn(
76
- `${device.ieee_address} state: ${stateID} error: value passed is not a valid JSON`
91
+ `${device.ieee_address} state: ${stateID} error: value passed is not a valid JSON – not sent`
77
92
  );
78
- this.adapter.log.debug(`${device.ieee_address} states: ${JSON.stringify(controlObj)} error: ${error}`);
93
+ this.adapter.log.debug(`${device.ieee_address} raw value: ${stateVal}`);
79
94
  return;
80
95
  }
81
96
  }
@@ -83,17 +98,14 @@ class Z2mController {
83
98
  // if available read option and set payload
84
99
  if (deviceState.options) {
85
100
  for (const option of deviceState.options) {
86
- // if optionsValues not set, set it!
87
- if (!device.optionsValues[option]) {
88
- const optionValue = (
89
- await this.adapter.getStateAsync(`${splitedID[0]}.${splitedID[1]}.${splitedID[2]}.${option}`)
90
- ).val;
91
- // optionsValues Cache
92
- device.optionsValues[option] = optionValue;
101
+ // Fix 2: Cache-Check mit === undefined statt !value
102
+ // Falsy-Check schlägt bei 0, false, null fehl (z.B. transition=0 = sofort)
103
+ if (device.optionsValues[option] === undefined) {
104
+ const optState = await this.adapter.getStateAsync(`${splitedID[0]}.${splitedID[1]}.${splitedID[2]}.${option}`);
105
+ device.optionsValues[option] = optState ? optState.val : null;
93
106
  }
94
107
 
95
- // if transition value == -1 it will be ignored. -1 stands for no overwrite!
96
- if (option == 'transition' && device.optionsValues[option] == -1) {
108
+ if (option === 'transition' && device.optionsValues[option] === -1) {
97
109
  continue;
98
110
  }
99
111
 
@@ -103,23 +115,17 @@ class Z2mController {
103
115
 
104
116
  // If an option datapoint has been set, it does not have to be sent.
105
117
  // This is confirmed directly by the adapter (ack = true)
106
- if (deviceState.isOption && deviceState.isOption == true) {
118
+ if (deviceState.isOption) {
107
119
  // set optionsValues 'Cache'
108
120
  device.optionsValues[stateName] = state.val;
109
121
  this.adapter.setState(id, state, true);
110
122
  return;
111
123
  }
112
124
 
113
- // set stats with the mentioned roles or always immediately to ack = true, because these are not reported back by Zigbee2MQTT
114
- if (['button'].includes(deviceState.role)) {
115
- this.adapter.setState(id, state, true);
116
- }
117
- // set stats with the mentioned ids always immediately to ack = true, because these are not reported back by Zigbee2MQTT
118
- if (
119
- ['brightness_move', 'colortemp_move', 'brightness_move', 'brightness_step', 'effect'].includes(
120
- deviceState.id
121
- )
122
- ) {
125
+ // States die nicht von Z2M zurückgemeldet werden sofort ack=true setzen
126
+ const immediateAckRole = ['button'].includes(deviceState.role);
127
+ const immediateAckId = ['brightness_move', 'colortemp_move', 'brightness_step', 'effect'].includes(deviceState.id);
128
+ if (immediateAckRole || immediateAckId) {
123
129
  this.adapter.setState(id, state, true);
124
130
  }
125
131
 
@@ -134,7 +140,10 @@ class Z2mController {
134
140
  * @param messageObj
135
141
  */
136
142
  async proxyZ2MLogs(messageObj) {
137
- const logMessage = messageObj.payload.message;
143
+ const logMessage = messageObj.payload && messageObj.payload.message;
144
+ if (!logMessage) {
145
+ return;
146
+ }
138
147
  if (this.logCustomizations.logfilter.some((x) => logMessage.includes(x))) {
139
148
  return;
140
149
  }