iobroker.zigbee2mqtt 3.1.1 → 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.
@@ -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
+ await this.adapter.setStateAsync(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
+ await this.adapter.setStateAsync(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
+ await this.adapter.setStateAsync(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,10 +69,9 @@ 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
- this.setState('info.connection', false, true);
74
+ await this.setStateAsync('info.connection', false, true);
61
75
 
62
76
  const debugDevicesState = await this.getStateAsync('info.debugmessages');
63
77
  if (debugDevicesState && debugDevicesState.val) {
@@ -110,7 +124,8 @@ class Zigbee2mqtt extends core.Adapter {
110
124
  // Internal MQTT-Server
111
125
  this.mqttServerController = new MqttServerController(this);
112
126
  await this.mqttServerController.createMQTTServer();
113
- await this.delay(1500);
127
+ // Kurze Pause damit der OS-Socket tatsächlich bereit ist (createMQTTServer wartet bereits auf listen)
128
+ await this.delay(200);
114
129
  this.mqttClient = mqtt.connect(`mqtt://${this.config.mqttServerIPBind}:${this.config.mqttServerPort}`, {
115
130
  clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
116
131
  clean: true,
@@ -123,7 +138,13 @@ class Zigbee2mqtt extends core.Adapter {
123
138
  this.log.info(
124
139
  `Connect to Zigbee2MQTT over ${this.config.connectionType === 'exmqtt' ? 'external mqtt' : 'internal mqtt'} connection.`
125
140
  );
126
- this.setStateChanged('info.connection', true, true);
141
+ this.setStateChangedAsync('info.connection', true, true).catch((e) =>
142
+ this.log.error(`MQTT connect setStateChangedAsync error: ${e}`)
143
+ );
144
+ if (!this.config.baseTopic) {
145
+ this.log.error('baseTopic is not configured – cannot subscribe to MQTT topics!');
146
+ return;
147
+ }
127
148
  this.mqttClient.subscribe(`${this.config.baseTopic}/#`, (err) => {
128
149
  if (err) {
129
150
  this.log.error(`MQTT subscribe error: ${err && err.message ? err.message : String(err)}`);
@@ -135,16 +156,18 @@ class Zigbee2mqtt extends core.Adapter {
135
156
  this.log.info('MQTT client reconnecting to Zigbee2MQTT...');
136
157
  });
137
158
 
138
- this.mqttClient.on('offline', async () => {
139
- this.log.warn('MQTT client offline – connection to Zigbee2MQTT lost.');
140
- this.setStateChanged('info.connection', false, true);
141
- try {
142
- if (this.statesController) {
143
- await this.statesController.setAllAvailableToFalse();
159
+ this.mqttClient.on('offline', () => {
160
+ (async () => {
161
+ this.log.warn('MQTT client offline – connection to Zigbee2MQTT lost.');
162
+ await this.setStateChangedAsync('info.connection', false, true);
163
+ try {
164
+ if (this.statesController) {
165
+ await this.statesController.setAllAvailableToFalse();
166
+ }
167
+ } catch (e) {
168
+ this.log.error(`MQTT offline setAllAvailableToFalse error: ${e}`);
144
169
  }
145
- } catch (e) {
146
- this.log.error(`MQTT offline setAllAvailableToFalse error: ${e}`);
147
- }
170
+ })().catch((e) => this.log.error(`MQTT offline handler error: ${e}`));
148
171
  });
149
172
 
150
173
  this.mqttClient.on('error', (err) => {
@@ -152,9 +175,9 @@ class Zigbee2mqtt extends core.Adapter {
152
175
  });
153
176
 
154
177
  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) {
178
+ // baseTopic-Prefix vollständig entfernen – funktioniert auch bei mehrstufigem baseTopic (z.B. home/zigbee2mqtt)
179
+ const basePrefix = this.config.baseTopic ? `${this.config.baseTopic}/` : null;
180
+ if (!basePrefix || !topic.startsWith(basePrefix)) {
158
181
  this.log.debug(`MQTT message with unexpected topic format: ${topic}`);
159
182
  return;
160
183
  }
@@ -167,7 +190,7 @@ class Zigbee2mqtt extends core.Adapter {
167
190
  }
168
191
  const messageObj = {
169
192
  payload: parsedPayload,
170
- topic: topic.slice(sepIdx + 1),
193
+ topic: topic.slice(basePrefix.length),
171
194
  };
172
195
  this.messageParse(messageObj).catch((err) => {
173
196
  this.log.error(`messageParse error: ${err}`);
@@ -183,13 +206,17 @@ class Zigbee2mqtt extends core.Adapter {
183
206
  if (this.config.dummyMqtt === true) {
184
207
  this.mqttServerController = new MqttServerController(this);
185
208
  await this.mqttServerController.createDummyMQTTServer();
186
- await this.delay(1500);
209
+ await this.delay(200);
187
210
  }
188
211
 
189
212
  this.startWebsocket();
190
213
  }
191
214
  }
192
215
 
216
+ /**
217
+ * Erstellt (einmalig) den WebsocketController und initiiert die WS-Verbindung.
218
+ * Wird beim Start und nach einem Verbindungsabbruch aufgerufen.
219
+ */
193
220
  startWebsocket() {
194
221
  // Controller nur einmal erstellen – bei Reconnect wird derselbe wiederverwendet
195
222
  if (!this.websocketController) {
@@ -198,6 +225,13 @@ class Zigbee2mqtt extends core.Adapter {
198
225
  this.websocketController.initWsClient();
199
226
  }
200
227
 
228
+ /**
229
+ * Parst eine eingehende MQTT- oder WebSocket-Nachricht von Zigbee2MQTT
230
+ * und leitet sie an den zuständigen Controller weiter.
231
+ * Alle Aufrufe werden serialisiert (Mutex), um Race-Conditions zu vermeiden.
232
+ *
233
+ * @param {{ topic: string, payload: any }} messageObj Die zu verarbeitende Nachricht
234
+ */
201
235
  async messageParse(messageObj) {
202
236
  // Mutex: serialisiert alle messageParse-Aufrufe
203
237
  // Wichtig: lock muss IMMER wieder freigegeben werden, auch bei early return / Fehler.
@@ -227,7 +261,7 @@ class Zigbee2mqtt extends core.Adapter {
227
261
  const coordState = typeof messageObj.payload === 'object' && messageObj.payload !== null
228
262
  ? messageObj.payload.state
229
263
  : messageObj.payload;
230
- this.setStateChanged('info.coordinator_status', coordState, true);
264
+ await this.setStateChangedAsync('info.coordinator_status', coordState, true);
231
265
  break;
232
266
  }
233
267
  case 'bridge/info':
@@ -247,7 +281,7 @@ class Zigbee2mqtt extends core.Adapter {
247
281
  await this.statesController.setAllAvailableToFalse();
248
282
  this.showInfo = true;
249
283
  }
250
- this.setStateChanged('info.connection', bridgeState === 'online', true);
284
+ await this.setStateChangedAsync('info.connection', bridgeState === 'online', true);
251
285
  break;
252
286
  }
253
287
  case 'bridge/devices':
@@ -318,12 +352,16 @@ class Zigbee2mqtt extends core.Adapter {
318
352
  if (messageObj.payload === null || messageObj.payload === undefined) {
319
353
  break;
320
354
  }
355
+ const deviceTopic = messageObj.topic.replace('/availability', '');
356
+ if (!deviceTopic) {
357
+ break;
358
+ }
321
359
  const availState = typeof messageObj.payload === 'object'
322
360
  ? messageObj.payload.state
323
361
  : messageObj.payload;
324
362
  const newMessage = {
325
363
  payload: { available: availState === 'online' },
326
- topic: messageObj.topic.replace('/availability', ''),
364
+ topic: deviceTopic,
327
365
  };
328
366
  await this.statesController.processDeviceMessage(newMessage);
329
367
  } else {
@@ -344,6 +382,11 @@ class Zigbee2mqtt extends core.Adapter {
344
382
  }
345
383
  }
346
384
 
385
+ /**
386
+ * Verarbeitet interne ioBroker-Nachrichten (z.B. Admin-UI-Kommandos).
387
+ *
388
+ * @param {ioBroker.Message} obj Das eingehende Nachrichtenobjekt
389
+ */
347
390
  async onMessage(obj) {
348
391
  if (!obj || !obj.command) {
349
392
  return;
@@ -384,6 +427,14 @@ class Zigbee2mqtt extends core.Adapter {
384
427
 
385
428
  // Alle vorhandenen Adapter-Objekte laden
386
429
  const allObjects = await this.getAdapterObjectsAsync();
430
+ if (!allObjects) {
431
+ const warn = 'deleteOldStates: getAdapterObjectsAsync returned null – aborting.';
432
+ this.log.error(warn);
433
+ if (obj.callback) {
434
+ this.sendTo(obj.from, obj.command, { error: warn }, obj.callback);
435
+ }
436
+ return;
437
+ }
387
438
  for (const id of Object.keys(allObjects)) {
388
439
  const iobObj = allObjects[id];
389
440
 
@@ -453,6 +504,12 @@ class Zigbee2mqtt extends core.Adapter {
453
504
  }
454
505
  }
455
506
 
507
+ /**
508
+ * Wird beim Stoppen des Adapters aufgerufen.
509
+ * Trennt alle Verbindungen, stoppt Timer und ruft abschließend callback() auf.
510
+ *
511
+ * @param {() => void} callback Muss am Ende zwingend aufgerufen werden
512
+ */
456
513
  async onUnload(callback) {
457
514
  try {
458
515
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
@@ -491,20 +548,20 @@ class Zigbee2mqtt extends core.Adapter {
491
548
  }
492
549
  try {
493
550
  if (this.websocketController) {
494
- await this.websocketController.allTimerClear();
551
+ this.websocketController.allTimerClear();
495
552
  }
496
553
  } catch (e) {
497
554
  this.log.error(e);
498
555
  }
499
556
  try {
500
557
  if (this.statesController) {
501
- await this.statesController.allTimerClear();
558
+ this.statesController.allTimerClear();
502
559
  }
503
560
  } catch (e) {
504
561
  this.log.error(e);
505
562
  }
506
563
 
507
- this.setState('info.connection', false, true);
564
+ await this.setStateAsync('info.connection', false, true);
508
565
 
509
566
  // Schedule-Job beenden
510
567
  const job = schedule.scheduledJobs['coordinatorCheck'];
@@ -518,16 +575,25 @@ class Zigbee2mqtt extends core.Adapter {
518
575
  }
519
576
  }
520
577
 
578
+ /**
579
+ * Reagiert auf Änderungen von ioBroker-States.
580
+ * Wandelt den State-Change in eine Zigbee2MQTT-Nachricht um und sendet sie.
581
+ *
582
+ * @param {string} id Vollständige State-ID (z.B. zigbee2mqtt.0.0xAABB.state)
583
+ * @param {ioBroker.State | null | undefined} state Das neue State-Objekt
584
+ */
521
585
  async onStateChange(id, state) {
522
586
  if (state && state.ack === false) {
523
587
  if (id.endsWith('info.debugmessages')) {
524
- this.logCustomizations.debugDevices = String(state.val || '');
525
- this.setState(id, state.val, true);
588
+ this.logCustomizations.debugDevices = state.val != null ? String(state.val) : '';
589
+ await this.setStateAsync(id, state.val, true);
526
590
  return;
527
591
  }
528
592
  if (id.endsWith('info.logfilter')) {
529
- this.logCustomizations.logfilter = String(state.val || '').split(';').filter((x) => x);
530
- this.setState(id, state.val, true);
593
+ this.logCustomizations.logfilter = state.val != null
594
+ ? String(state.val).split(';').filter((x) => x)
595
+ : [];
596
+ await this.setStateAsync(id, state.val, true);
531
597
  return;
532
598
  }
533
599
 
@@ -546,6 +612,10 @@ class Zigbee2mqtt extends core.Adapter {
546
612
  this.log.warn(`Cannot publish state, MQTT client not connected. (${id})`);
547
613
  return;
548
614
  }
615
+ if (!this.config.baseTopic) {
616
+ this.log.error('baseTopic is not configured – cannot publish state!');
617
+ return;
618
+ }
549
619
  try {
550
620
  this.mqttClient.publish(
551
621
  `${this.config.baseTopic}/${message.topic}`,
@@ -572,7 +642,7 @@ class Zigbee2mqtt extends core.Adapter {
572
642
 
573
643
  if (require.main !== module) {
574
644
  /**
575
- * @param {Partial<core.AdapterOptions>} [options]
645
+ * @param {object} [options] Optionale Adapter-Konfiguration (AdapterOptions)
576
646
  */
577
647
  module.exports = (options) => new Zigbee2mqtt(options);
578
648
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.zigbee2mqtt",
3
- "version": "3.1.1",
3
+ "version": "3.1.5",
4
4
  "description": "Zigbee2MQTT adapter for ioBroker",
5
5
  "author": {
6
6
  "name": "Dennis Rathjen and Arthur Rupp",