iobroker.zigbee2mqtt 3.2.6 → 3.2.7

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 CHANGED
@@ -30,6 +30,10 @@ check you VM Settings
30
30
  [Adapter Documentation](https://github.com/arteck/ioBroker.zigbee2mqtt/blob/main/docs/wiki.md)
31
31
 
32
32
  ## Changelog
33
+ ### 3.2.7 (2026-09-19)
34
+ * (arteck) fix available status direct after new device is paired
35
+ * (arteck) fix internal mqtt handling
36
+
33
37
  ### 3.2.6 (2026-08-20)
34
38
  * (arteck) add wait time for internal mqtt server into settings
35
39
 
@@ -45,9 +49,6 @@ check you VM Settings
45
49
  * (arteck) fix internal mqtt
46
50
  * (arteck) fix languages
47
51
 
48
- ### 3.2.2 (2026-05-26)
49
- * (arteck) Dependencies have been updated
50
-
51
52
  ## License
52
53
 
53
54
  MIT License
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zigbee2mqtt",
4
- "version": "3.2.6",
4
+ "version": "3.2.7",
5
5
  "news": {
6
+ "3.2.7": {
7
+ "en": "fix available status direct after new device is paired\nfix internal mqtt handling",
8
+ "de": "Verfügbarkeit direkt nach Kopplung korrigieren\nFix internes MQTT-Handling",
9
+ "ru": "исправить доступный статус сразу после сопряжения нового устройства\nисправить внутреннюю обработку mqtt",
10
+ "pt": "corrige o status disponível logo após o emparelhamento\nfixe o uso interno de matt",
11
+ "nl": "de beschikbare status herstellen direct nadat een nieuw apparaat is gekoppeld\ninterne mqtt-afhandeling repareren",
12
+ "fr": "corriger le statut disponible dès le jumelage d'un nouvel appareil\ngestion interne des tapis",
13
+ "it": "aggiusta subito dopo l'abbinamento del nuovo dispositivo\nfix gestione interna mqtt",
14
+ "es": "arreglar el estado disponible justo después de emparejar el nuevo\narreglar el manejo interno de MQTT",
15
+ "pl": "napraw dostępny status bezpośrednio po sparowaniu nowego urządzenia\nnaprawić wewnętrzną obsługę mqtt",
16
+ "uk": "виправлення доступного стану безпосередньо після створення пари нового пристрою\nвиправлено внутрішню обробку mqtt",
17
+ "zh-cn": "新设备配对后直接修复可用状态\n修复内部 mqtt 处理"
18
+ },
6
19
  "3.2.6": {
7
20
  "en": "add wait time for internal mqtt server into settings",
8
21
  "de": "Füge in den Einstellungen eine Wartezeit für den internen MQTT-Server hinzu.",
@@ -80,19 +93,6 @@
80
93
  "pl": "Adapter wymaga node.js > = 22\nmenedżer urządzenia upd\nfix aedes- trwałość\npodświetlenie",
81
94
  "uk": "Адаптер вимагає node.js >= 22 тепер\nдиспетчер пристроїв\nфіксувати адисистентність\nфіксувати ілюмінанс",
82
95
  "zh-cn": "适配器需要节点.js 现在22\n上调设备管理器\n固定死因-持久性\n修补灯光"
83
- },
84
- "3.2.0": {
85
- "en": "del deprectated setStateAsync",
86
- "de": "del deprected setStateAsync",
87
- "ru": "обсуждение StateAsync",
88
- "pt": "del deprectated setStateAsync",
89
- "nl": "del deprectated setStateAsync",
90
- "fr": "set dépréciéStateAsync",
91
- "it": "del set deprettatoStateAsync",
92
- "es": "del deprectated setStateAsync",
93
- "pl": "del deprectated setStateAsync",
94
- "uk": "deprectated setStateAsync",
95
- "zh-cn": "已贬值的设置"
96
96
  }
97
97
  },
98
98
  "messages": [
@@ -13,6 +13,13 @@ class ImageController {
13
13
  */
14
14
  constructor(adapter) {
15
15
  this.adapter = adapter;
16
+ // PERFORMANCE/MEMORY: In-Memory-Cache für bereits erzeugte Base64-Icons.
17
+ // Ohne diesen Cache würde bei JEDEM WS-Reconnect (Z2M sendet dann erneut
18
+ // das komplette bridge/devices-Payload) für JEDES Gerät erneut
19
+ // sharp(...).metadata()/.resize() aufgerufen – native libvips-Allokationen,
20
+ // die bei häufigen Reconnects zu spürbarem RAM-Zuwachs des Adapterprozesses
21
+ // führen können, da der native Speicher nicht sofort ans OS zurückgegeben wird.
22
+ this._iconCache = new Map();
16
23
  }
17
24
 
18
25
  /**
@@ -112,6 +119,14 @@ class ImageController {
112
119
  const z2mIconFileNamePNG = `${this.sanitizeZ2MDeviceName(z2mModel)}.png`;
113
120
  const slsIconFileName = `${this.sanitizeModelIDForImageUrl(device.model_id || z2mModel)}.png`;
114
121
 
122
+ // MEMORY-FIX: Bereits erzeugtes Icon (Base64) aus dem In-Memory-Cache verwenden.
123
+ // Der Cache-Key basiert auf Modellname + Zielgröße, da beides das Ergebnis beeinflusst.
124
+ const cacheKey = `${z2mModel}::${imageSize}`;
125
+ const cached = this._iconCache.get(cacheKey);
126
+ if (cached !== undefined) {
127
+ return cached;
128
+ }
129
+
115
130
  let iconFileName = await this.getExistingIconFileName(z2mIconFileNameJPG, z2mIconFileNamePNG, slsIconFileName);
116
131
  let iconFound = true;
117
132
 
@@ -137,6 +152,8 @@ class ImageController {
137
152
 
138
153
  if (!iconFound || !iconFileName) {
139
154
  this.adapter.log.warn(`Failed to download image for device model: ${device.definition.model} - ${device.definition.description}`);
155
+ // Fehlgeschlagene Downloads NICHT dauerhaft cachen – evtl. ist die Quelle beim
156
+ // nächsten Reconnect wieder erreichbar.
140
157
  return '';
141
158
  }
142
159
 
@@ -164,7 +181,11 @@ class ImageController {
164
181
  await this.adapter.writeFileAsync(this.adapter.namespace, iconFileName, icon.file);
165
182
  }
166
183
 
167
- return `data:image/png;base64,${icon.file.toString('base64')}`;
184
+ const dataUrl = `data:image/png;base64,${icon.file.toString('base64')}`;
185
+ // MEMORY-FIX: Ergebnis für dieses Modell/diese Größe cachen, damit bei künftigen
186
+ // WS-Reconnects (erneutes bridge/devices) kein weiterer sharp()-Aufruf nötig ist.
187
+ this._iconCache.set(cacheKey, dataUrl);
188
+ return dataUrl;
168
189
  }
169
190
 
170
191
  /**
@@ -23,6 +23,13 @@ class StatesController {
23
23
  this.logCustomizations = logCustomizations;
24
24
  this.createCache = createCache;
25
25
  this.incStatsQueue = [];
26
+ // Max. Verweildauer einer Nachricht in der incStatsQueue, bevor sie verworfen wird.
27
+ // Zeit- statt zählerbasiert, da neu angelernte Geräte während des Interviews mehrere
28
+ // bridge/devices-Zyklen (und damit processQueue()-Aufrufe) benötigen können, bis das
29
+ // Geräteobjekt/die States angelegt sind. Ein reiner Aufruf-Zähler hätte z.B. das
30
+ // "available: true" der Retained-".../availability"-Nachricht vorzeitig verworfen,
31
+ // wodurch der available-State neu angelernter Geräte dauerhaft auf "false" hängen blieb.
32
+ this.QUEUE_MAX_AGE_MS = 5 * 60 * 1000; // 5 Minuten
26
33
  this.timeOutCache = {};
27
34
  // Einmalig berechnen – wird nur bei Konfigurationsänderung ungültig
28
35
  this._debugDeviceList = logCustomizations.debugDevices
@@ -127,18 +134,28 @@ class StatesController {
127
134
  // Wenn das Gerät (noch) nicht bekannt ist: Message in Queue stellen.
128
135
  // Existiert für dieses Topic bereits ein Eintrag, wird er mit den aktuellen
129
136
  // Payload-Daten überschrieben, damit wir stets den neuesten Stand verarbeiten.
137
+ //
138
+ // Wichtig: Die Verwerf-Zeit basiert auf einem ZEITSTEMPEL (_queuedAt), nicht auf
139
+ // einem Aufruf-Zähler. Grund: Bei neu angelernten Geräten kann bridge/devices
140
+ // während des Interviews mehrfach gesendet werden (und processQueue() wird auch
141
+ // durch bridge/devices-Events ANDERER Geräte ausgelöst). Ein reiner Zähler
142
+ // würde die z.B. per Retained-MQTT-Topic ".../availability" bereits eingetroffene
143
+ // "available: true"-Nachricht des neuen Geräts vorzeitig verwerfen, bevor das
144
+ // Interview abgeschlossen und das Geräteobjekt angelegt ist – Folge: das
145
+ // "available"-State bleibt dauerhaft auf dem Default-Wert "false" stehen.
130
146
  const existingIdx = this.incStatsQueue.findIndex((x) => x && x.topic === messageObj.topic);
131
147
  if (existingIdx !== -1) {
132
- const ttl = (this.incStatsQueue[existingIdx]._ttl || 0) + 1;
133
- if (ttl > 10) {
134
- this.adapter.log.warn(`incStatsQueue: dropping message for unknown device ${messageObj.topic} after ${ttl} retries`);
148
+ const queuedAt = this.incStatsQueue[existingIdx]._queuedAt || Date.now();
149
+ const age = Date.now() - queuedAt;
150
+ if (age > this.QUEUE_MAX_AGE_MS) {
151
+ this.adapter.log.warn(`incStatsQueue: dropping message for unknown device ${messageObj.topic} after ${Math.round(age / 1000)}s`);
135
152
  this.incStatsQueue.splice(existingIdx, 1);
136
153
  return;
137
154
  }
138
- this.incStatsQueue[existingIdx] = { ...messageObj, _ttl: ttl };
155
+ this.incStatsQueue[existingIdx] = { ...messageObj, _queuedAt: queuedAt };
139
156
  } else {
140
157
  if (this.incStatsQueue.length < 500) {
141
- this.incStatsQueue.push({ ...messageObj, _ttl: 1 });
158
+ this.incStatsQueue.push({ ...messageObj, _queuedAt: Date.now() });
142
159
  } else {
143
160
  this.adapter.log.warn(`incStatsQueue is full (500), dropping message for ${messageObj.topic}`);
144
161
  }
@@ -168,15 +185,16 @@ class StatesController {
168
185
  if (queuedThisRound) {return;}
169
186
  const existingIdx = this.incStatsQueue.findIndex((x) => x && x.topic === msg.topic);
170
187
  if (existingIdx !== -1) {
171
- const ttl = (this.incStatsQueue[existingIdx]._ttl || 0) + 1;
172
- if (ttl > 10) {
173
- this.adapter.log.warn(`incStatsQueue: dropping message for ${msg.topic} after ${ttl} retries (state not yet created)`);
188
+ const queuedAt = this.incStatsQueue[existingIdx]._queuedAt || Date.now();
189
+ const age = Date.now() - queuedAt;
190
+ if (age > this.QUEUE_MAX_AGE_MS) {
191
+ this.adapter.log.warn(`incStatsQueue: dropping message for ${msg.topic} after ${Math.round(age / 1000)}s (state not yet created)`);
174
192
  this.incStatsQueue.splice(existingIdx, 1);
175
193
  return;
176
194
  }
177
- this.incStatsQueue[existingIdx] = { ...msg, _ttl: ttl };
195
+ this.incStatsQueue[existingIdx] = { ...msg, _queuedAt: queuedAt };
178
196
  } else if (this.incStatsQueue.length < 500) {
179
- this.incStatsQueue.push({ ...msg, _ttl: 1 });
197
+ this.incStatsQueue.push({ ...msg, _queuedAt: Date.now() });
180
198
  } else {
181
199
  this.adapter.log.warn(`incStatsQueue is full, dropping message for ${msg.topic}`);
182
200
  }
@@ -213,9 +231,25 @@ class StatesController {
213
231
 
214
232
  // COLLECTION: Alle non-action state write-Promises sammeln und dann parallel ausführen
215
233
  const stateWrites = [];
216
- // Flag für available-Update (wenn last_seen vorhanden und config aktiv)
234
+ // Flag für available-Update (wenn last_seen vorhanden und config aktiv, oder generell
235
+ // wenn echte Nutzdaten empfangen wurden – siehe Begründung weiter unten)
217
236
  let needSetAvailableTrue = false;
218
237
 
238
+ // Bugfix: Wenn eine Nachricht ausschließlich das "available"-Property enthält
239
+ // (z.B. aus dem ".../availability"-Topic oder bridge/event device_leave), soll
240
+ // dessen Wert unverändert übernommen werden. Enthält die Nachricht jedoch
241
+ // zusätzliche/echte Nutzdaten (state, brightness, temperature, ...), beweist das
242
+ // bereits, dass das Gerät aktiv kommuniziert hat – unabhängig davon, ob Zigbee2MQTT
243
+ // das (per Default deaktivierte) "availability"-Feature nutzt oder last_seen sendet
244
+ // (advanced.last_seen ist bei Z2M ebenfalls per Default "disable"). Ohne diesen Fix
245
+ // bleibt der "available"-State nach einem MQTT-/Adapter-Neustart dauerhaft auf
246
+ // "false", obwohl die Geräte normal erreichbar und schaltbar sind.
247
+ const payloadKeys = Object.keys(messageObj.payload);
248
+ const hasOnlyAvailableKey = payloadKeys.length > 0 && payloadKeys.every((k) => k === 'available');
249
+ if (!hasOnlyAvailableKey && payloadKeys.length > 0 && !device.ieee_address.includes('group')) {
250
+ needSetAvailableTrue = true;
251
+ }
252
+
219
253
  for (let [key, value] of Object.entries(messageObj.payload)) {
220
254
  if (value === undefined || value === null) {
221
255
  continue;
package/main.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // you need to create an adapter
5
5
  const core = require('@iobroker/adapter-core');
6
6
  const mqtt = require('mqtt');
7
+ const axios = require('axios');
7
8
  const utils = require('./lib/utils');
8
9
  const schedule = require('node-schedule');
9
10
  const checkConfig = require('./lib/check').checkConfig;
@@ -41,6 +42,14 @@ class Zigbee2mqtt extends core.Adapter {
41
42
  this.mqttServerController = null;
42
43
  this.messageParseMutex = Promise.resolve();
43
44
  this.mqttReconnectAttempts = 0;
45
+ // Fallback: Wenn nach dem Verbindungsaufbau kein bridge/devices eintrifft
46
+ // (z.B. weil bei "intmqtt" der interne Broker samt Retained-Nachrichten neu
47
+ // erzeugt wurde und Zigbee2MQTT diese nicht von sich aus erneut published),
48
+ // wird EINMALIG versucht, die Geräteliste über die Z2M-WebUI-REST-API
49
+ // nachzuladen. Es wird NIE erneut onReady()/mqtt.connect() aufgerufen, um
50
+ // doppelte Clients/Listener bzw. Port-Konflikte (EADDRINUSE bei intmqtt) zu
51
+ // vermeiden.
52
+ this.deviceReloadTimer = null;
44
53
 
45
54
  this.on('ready', () => {
46
55
  this.onReady().catch((e) => this.log.error(`onReady error: ${e}`));
@@ -159,6 +168,10 @@ class Zigbee2mqtt extends core.Adapter {
159
168
  this.log.error(`MQTT subscribe error: ${err && err.message ? err.message : String(err)}`);
160
169
  }
161
170
  });
171
+ // Einmaliger Fallback-Check: kommt innerhalb von 5s kein bridge/devices
172
+ // (z.B. verlorene Retained-Messages bei intmqtt-Neustart), versuche die
173
+ // Geräteliste über die Z2M-WebUI-REST-API nachzuladen.
174
+ this.scheduleDeviceReloadFallback();
162
175
  });
163
176
 
164
177
  this.mqttClient.on('reconnect', () => {
@@ -259,6 +272,69 @@ class Zigbee2mqtt extends core.Adapter {
259
272
  this.websocketController.initWsClient();
260
273
  }
261
274
 
275
+ /**
276
+ * Plant einen einmaligen Fallback-Timer: Kommt innerhalb von 5 Sekunden kein
277
+ * bridge/devices von Zigbee2MQTT an (deviceCache bleibt leer), wird versucht,
278
+ * die Geräteliste über die Z2M-WebUI-REST-API (`/api/devices`) nachzuladen.
279
+ * Wird KEIN neuer MQTT-Client / Broker erzeugt – nur ein einmaliger HTTP-Call.
280
+ */
281
+ scheduleDeviceReloadFallback() {
282
+ if (this.deviceReloadTimer) {
283
+ clearTimeout(this.deviceReloadTimer);
284
+ }
285
+ this.deviceReloadTimer = setTimeout(() => {
286
+ this.deviceReloadTimer = null;
287
+ this.tryFallbackDeviceReload().catch((e) => this.log.error(`tryFallbackDeviceReload error: ${e}`));
288
+ }, 5000);
289
+ }
290
+
291
+ /**
292
+ * Lädt die Geräteliste einmalig über die Z2M-WebUI-REST-API nach, falls nach
293
+ * dem MQTT-Connect innerhalb von 5s kein bridge/devices empfangen wurde.
294
+ * Wird u.a. benötigt, wenn bei "intmqtt" der interne Broker (In-Memory-Persistenz)
295
+ * neu erzeugt wurde und Zigbee2MQTT die Retained-Nachrichten nicht von sich aus
296
+ * erneut published (Z2M selbst läuft weiter, ohne eigenen Neustart).
297
+ */
298
+ async tryFallbackDeviceReload() {
299
+ if (this.deviceCache.length > 0) {
300
+ // bridge/devices ist zwischenzeitlich normal eingetroffen – nichts zu tun
301
+ return;
302
+ }
303
+ if (!this.config.webUIServer || !this.config.webUIPort) {
304
+ this.log.debug('Fallback device reload skipped – WebUI address/port not configured.');
305
+ return;
306
+ }
307
+ this.log.warn(
308
+ 'No bridge/devices received from Zigbee2MQTT within 5s – trying fallback via WebUI REST API. ' +
309
+ 'This can happen if the internal MQTT broker was recreated (adapter restart) while Zigbee2MQTT itself keeps running.'
310
+ );
311
+ try {
312
+ const scheme = this.config.webUIScheme || 'http';
313
+ const url = `${scheme}://${this.config.webUIServer}:${this.config.webUIPort}/api/devices`;
314
+ const response = await axios.get(url, { timeout: 5000 });
315
+ const devices = response && response.data;
316
+ if (!Array.isArray(devices) || devices.length === 0) {
317
+ this.log.warn('Fallback device reload: WebUI API returned no devices.');
318
+ return;
319
+ }
320
+ if (!this.deviceController || !this.statesController) {
321
+ this.log.debug('Fallback device reload: controllers not yet initialized, aborting.');
322
+ return;
323
+ }
324
+ await this.deviceController.createDeviceDefinitions(devices);
325
+ await this.deviceController.createOrUpdateDevices();
326
+ await this.statesController.subscribeWritableStates();
327
+ await this.statesController.processQueue();
328
+ this.log.info(`Fallback device reload completed – ${devices.length} device(s) imported via WebUI API.`);
329
+ } catch (e) {
330
+ this.log.error(
331
+ `Fallback device reload via WebUI API failed: ${e && e.message ? e.message : String(e)}. ` +
332
+ 'Please verify that the Zigbee2MQTT frontend is enabled and reachable, or restart Zigbee2MQTT once ' +
333
+ 'to force it to re-publish bridge/devices.'
334
+ );
335
+ }
336
+ }
337
+
262
338
  /**
263
339
  * Parst eine eingehende MQTT- oder WebSocket-Nachricht von Zigbee2MQTT
264
340
  * und leitet sie an den zuständigen Controller weiter.
@@ -319,6 +395,10 @@ class Zigbee2mqtt extends core.Adapter {
319
395
  break;
320
396
  }
321
397
  case 'bridge/devices':
398
+ if (this.deviceReloadTimer) {
399
+ clearTimeout(this.deviceReloadTimer);
400
+ this.deviceReloadTimer = null;
401
+ }
322
402
  await this.deviceController.createDeviceDefinitions(messageObj.payload);
323
403
  await this.deviceController.createOrUpdateDevices();
324
404
  await this.deviceController.checkAndProgressDeviceRemove();
@@ -546,6 +626,10 @@ class Zigbee2mqtt extends core.Adapter {
546
626
  */
547
627
  async onUnload(callback) {
548
628
  try {
629
+ if (this.deviceReloadTimer) {
630
+ clearTimeout(this.deviceReloadTimer);
631
+ this.deviceReloadTimer = null;
632
+ }
549
633
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
550
634
  if (this.mqttClient && this.mqttClient.connected) {
551
635
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.zigbee2mqtt",
3
- "version": "3.2.6",
3
+ "version": "3.2.7",
4
4
  "description": "Zigbee2MQTT adapter for ioBroker",
5
5
  "author": {
6
6
  "name": "Dennis Rathjen and Arthur Rupp",
@@ -25,13 +25,13 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@iobroker/adapter-core": "^3.4.3",
28
- "@iobroker/dm-utils": "^3.1.3",
28
+ "@iobroker/dm-utils": "^3.2.0",
29
29
  "aedes": "^0.51.3",
30
30
  "aedes-persistence": "^9.1.2",
31
- "axios": "^1.18.1",
31
+ "axios": "^1.20.0",
32
32
  "mqtt": "^5.15.2",
33
33
  "node-schedule": "^2.1.1",
34
- "sharp": "^0.34.5",
34
+ "sharp": "^0.35.4",
35
35
  "ws": "^8.21.3"
36
36
  },
37
37
  "devDependencies": {
@@ -41,9 +41,9 @@
41
41
  "@alcalzone/release-script-plugin-manual-review": "^5.2.0",
42
42
  "@iobroker/adapter-dev": "^1.5.0",
43
43
  "@iobroker/eslint-config": "^2.3.4",
44
- "@iobroker/testing": "^5.3.0",
45
- "@tsconfig/node22": "^22.0.5",
46
- "@types/node": "^25.9.5",
44
+ "@iobroker/testing": "^6.2.1",
45
+ "@tsconfig/node22": "^22.0.6",
46
+ "@types/node": "^25.9.6",
47
47
  "@types/node-schedule": "^2.1.8",
48
48
  "typescript": "~6.0.3"
49
49
  },