iobroker.zigbee2mqtt 3.1.1 → 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.
@@ -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('.');
@@ -82,7 +87,7 @@ class Z2mController {
82
87
  if (stateID === 'send_payload') {
83
88
  try {
84
89
  controlObj.payload = JSON.parse(stateVal);
85
- this.adapter.setState(id, state, true);
90
+ this.adapter.setState(id, state.val, true);
86
91
  } catch (error) {
87
92
  // Fix 9: rawStr als String direkt als payload – wird in main.js via
88
93
  // JSON.stringify() nochmals gewrappt → würde doppelt quoten.
@@ -98,9 +103,8 @@ class Z2mController {
98
103
  // if available read option and set payload
99
104
  if (deviceState.options) {
100
105
  for (const option of deviceState.options) {
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) {
106
+ // Fix: "in"-Prüfung statt === undefined, damit null als gültiger gecachter Wert behandelt wird
107
+ if (!(option in device.optionsValues)) {
104
108
  const optState = await this.adapter.getStateAsync(`${splitedID[0]}.${splitedID[1]}.${splitedID[2]}.${option}`);
105
109
  device.optionsValues[option] = optState ? optState.val : null;
106
110
  }
@@ -118,7 +122,7 @@ class Z2mController {
118
122
  if (deviceState.isOption) {
119
123
  // set optionsValues 'Cache'
120
124
  device.optionsValues[stateName] = state.val;
121
- this.adapter.setState(id, state, true);
125
+ this.adapter.setState(id, state.val, true);
122
126
  return;
123
127
  }
124
128
 
@@ -126,18 +130,23 @@ class Z2mController {
126
130
  const immediateAckRole = ['button'].includes(deviceState.role);
127
131
  const immediateAckId = ['brightness_move', 'colortemp_move', 'brightness_step', 'effect'].includes(deviceState.id);
128
132
  if (immediateAckRole || immediateAckId) {
129
- this.adapter.setState(id, state, true);
133
+ this.adapter.setState(id, state.val, true);
130
134
  }
131
135
 
132
- if (this.logCustomizations.debugDevices.includes(device.ieee_address)) {
133
- this.adapter.log.warn(`<<<--- toZ2M -> ${device.ieee_address} states: ${JSON.stringify(controlObj)}`);
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
+ }
134
141
  }
135
142
  return controlObj;
136
143
  }
137
144
 
138
145
  /**
146
+ * Leitet eine Z2M-Log-Nachricht (bridge/logging) an den ioBroker-Logger weiter.
147
+ * Nachrichten die dem konfigurierten Logfilter entsprechen werden unterdrückt.
139
148
  *
140
- * @param messageObj
149
+ * @param {{ payload: { message: string, level: string } }} messageObj Die Log-Nachricht
141
150
  */
142
151
  async proxyZ2MLogs(messageObj) {
143
152
  const logMessage = messageObj.payload && messageObj.payload.message;
@@ -158,6 +167,9 @@ class Z2mController {
158
167
  case 'warning':
159
168
  this.adapter.log.warn(logMessage);
160
169
  break;
170
+ default:
171
+ this.adapter.log.debug(`Z2M [${logLevel}]: ${logMessage}`);
172
+ break;
161
173
  }
162
174
  }
163
175
  }
package/main.js CHANGED
@@ -16,6 +16,11 @@ const WebsocketController = require('./lib/websocketController').WebsocketContro
16
16
  const MqttServerController = require('./lib/mqttServerController').MqttServerController;
17
17
 
18
18
  class Zigbee2mqtt extends core.Adapter {
19
+ /**
20
+ * Erstellt eine neue Instanz des Zigbee2MQTT-Adapters.
21
+ *
22
+ * @param {object} [options] Optionale Adapter-Konfiguration (AdapterOptions)
23
+ */
19
24
  constructor(options) {
20
25
  super({
21
26
  ...options,
@@ -36,12 +41,22 @@ class Zigbee2mqtt extends core.Adapter {
36
41
  this.mqttServerController = null;
37
42
  this.messageParseMutex = Promise.resolve();
38
43
 
39
- this.on('ready', this.onReady.bind(this));
40
- this.on('stateChange', this.onStateChange.bind(this));
41
- this.on('message', this.onMessage.bind(this));
44
+ this.on('ready', () => {
45
+ this.onReady().catch((e) => this.log.error(`onReady error: ${e}`));
46
+ });
47
+ this.on('stateChange', (id, state) => {
48
+ this.onStateChange(id, state).catch((e) => this.log.error(`onStateChange error: ${e}`));
49
+ });
50
+ this.on('message', (obj) => {
51
+ this.onMessage(obj).catch((e) => this.log.error(`onMessage error: ${e}`));
52
+ });
42
53
  this.on('unload', this.onUnload.bind(this));
43
54
  }
44
55
 
56
+ /**
57
+ * Wird aufgerufen sobald der Adapter bereit ist (alle Objekte initialisiert).
58
+ * Initialisiert alle Controller und baut die Verbindung zu Zigbee2MQTT auf.
59
+ */
45
60
  async onReady() {
46
61
  this.statesController = new StatesController(this, this.deviceCache, this.groupCache, this.logCustomizations, this.createCache);
47
62
  this.deviceController = new DeviceController(
@@ -54,8 +69,7 @@ class Zigbee2mqtt extends core.Adapter {
54
69
  );
55
70
  this.z2mController = new Z2mController(this, this.deviceCache, this.groupCache, this.logCustomizations);
56
71
 
57
- // Fix 2: adapterInfo ist async → awaiten
58
- await adapterInfo(this.config, this.log);
72
+ adapterInfo(this.config, this.log);
59
73
 
60
74
  this.setState('info.connection', false, true);
61
75
 
@@ -95,6 +109,7 @@ class Zigbee2mqtt extends core.Adapter {
95
109
  clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
96
110
  clean: true,
97
111
  reconnectPeriod: 500,
112
+ connectTimeout: 10000,
98
113
  };
99
114
 
100
115
  if (this.config.externalMqttServerCredentials === true) {
@@ -110,11 +125,13 @@ class Zigbee2mqtt extends core.Adapter {
110
125
  // Internal MQTT-Server
111
126
  this.mqttServerController = new MqttServerController(this);
112
127
  await this.mqttServerController.createMQTTServer();
113
- await this.delay(1500);
128
+ // Kurze Pause damit der OS-Socket tatsächlich bereit ist (createMQTTServer wartet bereits auf listen)
129
+ await this.delay(200);
114
130
  this.mqttClient = mqtt.connect(`mqtt://${this.config.mqttServerIPBind}:${this.config.mqttServerPort}`, {
115
131
  clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
116
132
  clean: true,
117
133
  reconnectPeriod: 500,
134
+ connectTimeout: 10000,
118
135
  });
119
136
  }
120
137
 
@@ -123,7 +140,13 @@ class Zigbee2mqtt extends core.Adapter {
123
140
  this.log.info(
124
141
  `Connect to Zigbee2MQTT over ${this.config.connectionType === 'exmqtt' ? 'external mqtt' : 'internal mqtt'} connection.`
125
142
  );
126
- this.setStateChanged('info.connection', true, true);
143
+ this.setStateChangedAsync('info.connection', true, true).catch((e) =>
144
+ this.log.error(`MQTT connect setStateChangedAsync error: ${e}`)
145
+ );
146
+ if (!this.config.baseTopic) {
147
+ this.log.error('baseTopic is not configured – cannot subscribe to MQTT topics!');
148
+ return;
149
+ }
127
150
  this.mqttClient.subscribe(`${this.config.baseTopic}/#`, (err) => {
128
151
  if (err) {
129
152
  this.log.error(`MQTT subscribe error: ${err && err.message ? err.message : String(err)}`);
@@ -137,7 +160,9 @@ class Zigbee2mqtt extends core.Adapter {
137
160
 
138
161
  this.mqttClient.on('offline', async () => {
139
162
  this.log.warn('MQTT client offline – connection to Zigbee2MQTT lost.');
140
- this.setStateChanged('info.connection', false, true);
163
+ await this.setStateChangedAsync('info.connection', false, true).catch((e) =>
164
+ this.log.error(`MQTT offline setStateChangedAsync error: ${e}`)
165
+ );
141
166
  try {
142
167
  if (this.statesController) {
143
168
  await this.statesController.setAllAvailableToFalse();
@@ -147,14 +172,21 @@ class Zigbee2mqtt extends core.Adapter {
147
172
  }
148
173
  });
149
174
 
175
+ this.mqttClient.on('close', async () => {
176
+ this.log.debug('MQTT client connection closed.');
177
+ await this.setStateChangedAsync('info.connection', false, true).catch((e) =>
178
+ this.log.error(`MQTT close setStateChangedAsync error: ${e}`)
179
+ );
180
+ });
181
+
150
182
  this.mqttClient.on('error', (err) => {
151
183
  this.log.error(`MQTT client error: ${err && err.message ? err.message : String(err)}`);
152
184
  });
153
185
 
154
186
  this.mqttClient.on('message', (topic, payload) => {
155
- // baseTopic aus dem Topic entfernen – Guard falls kein '/' vorhanden
156
- const sepIdx = topic.indexOf('/');
157
- if (sepIdx === -1) {
187
+ // baseTopic-Prefix vollständig entfernen – funktioniert auch bei mehrstufigem baseTopic (z.B. home/zigbee2mqtt)
188
+ const basePrefix = this.config.baseTopic ? `${this.config.baseTopic}/` : null;
189
+ if (!basePrefix || !topic.startsWith(basePrefix)) {
158
190
  this.log.debug(`MQTT message with unexpected topic format: ${topic}`);
159
191
  return;
160
192
  }
@@ -167,7 +199,7 @@ class Zigbee2mqtt extends core.Adapter {
167
199
  }
168
200
  const messageObj = {
169
201
  payload: parsedPayload,
170
- topic: topic.slice(sepIdx + 1),
202
+ topic: topic.slice(basePrefix.length),
171
203
  };
172
204
  this.messageParse(messageObj).catch((err) => {
173
205
  this.log.error(`messageParse error: ${err}`);
@@ -183,13 +215,17 @@ class Zigbee2mqtt extends core.Adapter {
183
215
  if (this.config.dummyMqtt === true) {
184
216
  this.mqttServerController = new MqttServerController(this);
185
217
  await this.mqttServerController.createDummyMQTTServer();
186
- await this.delay(1500);
218
+ await this.delay(200);
187
219
  }
188
220
 
189
221
  this.startWebsocket();
190
222
  }
191
223
  }
192
224
 
225
+ /**
226
+ * Erstellt (einmalig) den WebsocketController und initiiert die WS-Verbindung.
227
+ * Wird beim Start und nach einem Verbindungsabbruch aufgerufen.
228
+ */
193
229
  startWebsocket() {
194
230
  // Controller nur einmal erstellen – bei Reconnect wird derselbe wiederverwendet
195
231
  if (!this.websocketController) {
@@ -198,6 +234,13 @@ class Zigbee2mqtt extends core.Adapter {
198
234
  this.websocketController.initWsClient();
199
235
  }
200
236
 
237
+ /**
238
+ * Parst eine eingehende MQTT- oder WebSocket-Nachricht von Zigbee2MQTT
239
+ * und leitet sie an den zuständigen Controller weiter.
240
+ * Alle Aufrufe werden serialisiert (Mutex), um Race-Conditions zu vermeiden.
241
+ *
242
+ * @param {{ topic: string, payload: any }} messageObj Die zu verarbeitende Nachricht
243
+ */
201
244
  async messageParse(messageObj) {
202
245
  // Mutex: serialisiert alle messageParse-Aufrufe
203
246
  // Wichtig: lock muss IMMER wieder freigegeben werden, auch bei early return / Fehler.
@@ -227,7 +270,7 @@ class Zigbee2mqtt extends core.Adapter {
227
270
  const coordState = typeof messageObj.payload === 'object' && messageObj.payload !== null
228
271
  ? messageObj.payload.state
229
272
  : messageObj.payload;
230
- this.setStateChanged('info.coordinator_status', coordState, true);
273
+ await this.setStateChangedAsync('info.coordinator_status', coordState, true);
231
274
  break;
232
275
  }
233
276
  case 'bridge/info':
@@ -247,7 +290,7 @@ class Zigbee2mqtt extends core.Adapter {
247
290
  await this.statesController.setAllAvailableToFalse();
248
291
  this.showInfo = true;
249
292
  }
250
- this.setStateChanged('info.connection', bridgeState === 'online', true);
293
+ await this.setStateChangedAsync('info.connection', bridgeState === 'online', true);
251
294
  break;
252
295
  }
253
296
  case 'bridge/devices':
@@ -318,12 +361,16 @@ class Zigbee2mqtt extends core.Adapter {
318
361
  if (messageObj.payload === null || messageObj.payload === undefined) {
319
362
  break;
320
363
  }
364
+ const deviceTopic = messageObj.topic.replace('/availability', '');
365
+ if (!deviceTopic) {
366
+ break;
367
+ }
321
368
  const availState = typeof messageObj.payload === 'object'
322
369
  ? messageObj.payload.state
323
370
  : messageObj.payload;
324
371
  const newMessage = {
325
372
  payload: { available: availState === 'online' },
326
- topic: messageObj.topic.replace('/availability', ''),
373
+ topic: deviceTopic,
327
374
  };
328
375
  await this.statesController.processDeviceMessage(newMessage);
329
376
  } else {
@@ -344,6 +391,11 @@ class Zigbee2mqtt extends core.Adapter {
344
391
  }
345
392
  }
346
393
 
394
+ /**
395
+ * Verarbeitet interne ioBroker-Nachrichten (z.B. Admin-UI-Kommandos).
396
+ *
397
+ * @param {ioBroker.Message} obj Das eingehende Nachrichtenobjekt
398
+ */
347
399
  async onMessage(obj) {
348
400
  if (!obj || !obj.command) {
349
401
  return;
@@ -384,6 +436,14 @@ class Zigbee2mqtt extends core.Adapter {
384
436
 
385
437
  // Alle vorhandenen Adapter-Objekte laden
386
438
  const allObjects = await this.getAdapterObjectsAsync();
439
+ if (!allObjects) {
440
+ const warn = 'deleteOldStates: getAdapterObjectsAsync returned null – aborting.';
441
+ this.log.error(warn);
442
+ if (obj.callback) {
443
+ this.sendTo(obj.from, obj.command, { error: warn }, obj.callback);
444
+ }
445
+ return;
446
+ }
387
447
  for (const id of Object.keys(allObjects)) {
388
448
  const iobObj = allObjects[id];
389
449
 
@@ -453,10 +513,16 @@ class Zigbee2mqtt extends core.Adapter {
453
513
  }
454
514
  }
455
515
 
516
+ /**
517
+ * Wird beim Stoppen des Adapters aufgerufen.
518
+ * Trennt alle Verbindungen, stoppt Timer und ruft abschließend callback() auf.
519
+ *
520
+ * @param {() => void} callback Muss am Ende zwingend aufgerufen werden
521
+ */
456
522
  async onUnload(callback) {
457
523
  try {
458
524
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
459
- if (this.mqttClient && !this.mqttClient.disconnected) {
525
+ if (this.mqttClient && this.mqttClient.connected) {
460
526
  try {
461
527
  this.mqttClient.removeAllListeners();
462
528
  this.mqttClient.end(true);
@@ -491,14 +557,14 @@ class Zigbee2mqtt extends core.Adapter {
491
557
  }
492
558
  try {
493
559
  if (this.websocketController) {
494
- await this.websocketController.allTimerClear();
560
+ this.websocketController.allTimerClear();
495
561
  }
496
562
  } catch (e) {
497
563
  this.log.error(e);
498
564
  }
499
565
  try {
500
566
  if (this.statesController) {
501
- await this.statesController.allTimerClear();
567
+ this.statesController.allTimerClear();
502
568
  }
503
569
  } catch (e) {
504
570
  this.log.error(e);
@@ -518,15 +584,24 @@ class Zigbee2mqtt extends core.Adapter {
518
584
  }
519
585
  }
520
586
 
587
+ /**
588
+ * Reagiert auf Änderungen von ioBroker-States.
589
+ * Wandelt den State-Change in eine Zigbee2MQTT-Nachricht um und sendet sie.
590
+ *
591
+ * @param {string} id Vollständige State-ID (z.B. zigbee2mqtt.0.0xAABB.state)
592
+ * @param {ioBroker.State | null | undefined} state Das neue State-Objekt
593
+ */
521
594
  async onStateChange(id, state) {
522
595
  if (state && state.ack === false) {
523
596
  if (id.endsWith('info.debugmessages')) {
524
- this.logCustomizations.debugDevices = String(state.val || '');
597
+ this.logCustomizations.debugDevices = state.val != null ? String(state.val) : '';
525
598
  this.setState(id, state.val, true);
526
599
  return;
527
600
  }
528
601
  if (id.endsWith('info.logfilter')) {
529
- this.logCustomizations.logfilter = String(state.val || '').split(';').filter((x) => x);
602
+ this.logCustomizations.logfilter = state.val != null
603
+ ? String(state.val).split(';').filter((x) => x)
604
+ : [];
530
605
  this.setState(id, state.val, true);
531
606
  return;
532
607
  }
@@ -542,10 +617,14 @@ class Zigbee2mqtt extends core.Adapter {
542
617
  }
543
618
 
544
619
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
545
- if (!this.mqttClient || this.mqttClient.disconnected) {
620
+ if (!this.mqttClient || !this.mqttClient.connected) {
546
621
  this.log.warn(`Cannot publish state, MQTT client not connected. (${id})`);
547
622
  return;
548
623
  }
624
+ if (!this.config.baseTopic) {
625
+ this.log.error('baseTopic is not configured – cannot publish state!');
626
+ return;
627
+ }
549
628
  try {
550
629
  this.mqttClient.publish(
551
630
  `${this.config.baseTopic}/${message.topic}`,
@@ -572,7 +651,7 @@ class Zigbee2mqtt extends core.Adapter {
572
651
 
573
652
  if (require.main !== module) {
574
653
  /**
575
- * @param {Partial<core.AdapterOptions>} [options]
654
+ * @param {object} [options] Optionale Adapter-Konfiguration (AdapterOptions)
576
655
  */
577
656
  module.exports = (options) => new Zigbee2mqtt(options);
578
657
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.zigbee2mqtt",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Zigbee2MQTT adapter for ioBroker",
5
5
  "author": {
6
6
  "name": "Dennis Rathjen and Arthur Rupp",
@@ -26,7 +26,7 @@
26
26
  "dependencies": {
27
27
  "@iobroker/adapter-core": "^3.3.2",
28
28
  "@iobroker/dm-utils": "^3.0.17",
29
- "aedes": "^1.0.2",
29
+ "aedes": "^0.51.3",
30
30
  "aedes-persistence-nedb": "^2.0.3",
31
31
  "mqtt": "^5.15.1",
32
32
  "axios": "^1.14.0",