iobroker.zigbee2mqtt 3.1.5 → 3.2.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.
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const core = require('@iobroker/adapter-core');
4
- const { Aedes } = require('aedes');
3
+ const Aedes = require('aedes');
5
4
  const net = require('node:net');
6
5
 
7
6
  /**
@@ -51,40 +50,37 @@ class MqttServerController {
51
50
  }
52
51
 
53
52
  /**
54
- * Startet den internen MQTT-Server mit nedb-Persistenz (aedes + aedes-persistence-nedb).
53
+ * Startet den internen MQTT-Server mit In-Memory-Persistenz (aedes + aedes-persistence-memory).
55
54
  * Wartet auf den erfolgreichen listen()-Aufruf bevor die Methode zurückkehrt.
55
+ * Hinweis: aedes-persistence-nedb wurde entfernt, da es das veraltete nedb-Paket
56
+ * mit util.isDate verwendet (entfernt in Node.js v12+). In-Memory ist für einen
57
+ * internen Broker ausreichend – Clients subscriben sich bei Reconnect automatisch neu.
56
58
  */
57
59
  async createMQTTServer() {
58
60
  if (!this._checkConfig()) {
59
61
  return;
60
62
  }
61
63
  try {
62
- const NedbPersistence = require('aedes-persistence-nedb');
63
- const db = new NedbPersistence({
64
- path: `${core.getAbsoluteInstanceDataDir(this.adapter)}/mqttData`,
65
- prefix: '',
66
- });
67
- // aedes-persistence-nedb does not implement the async setup() interface
68
- // required by aedes v1.x → no-op shim
69
- if (!db.setup) {
70
- db.setup = async () => {};
71
- }
72
- this.aedesBroker = await Aedes.createBroker({ persistence: db });
73
- this.mqttServer = net.createServer(this.aedesBroker.handle);
64
+ const MemoryPersistence = require('aedes-persistence');
65
+ const db = MemoryPersistence();
66
+ const aedes = Aedes({ persistence: db });
67
+ this.aedesBroker = aedes;
68
+ this.mqttServer = net.createServer(aedes.handle);
74
69
  this._attachServerEvents('MQTT-Server');
75
70
 
76
71
  await new Promise((resolve, reject) => {
72
+ this.mqttServer.once('error', reject);
77
73
  this.mqttServer.listen(
78
74
  this.adapter.config.mqttServerPort,
79
75
  this.adapter.config.mqttServerIPBind,
80
76
  () => {
77
+ this.mqttServer.removeListener('error', reject);
81
78
  this.adapter.log.info(
82
79
  `Starting MQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
83
80
  );
84
81
  resolve();
85
82
  }
86
83
  );
87
- this.mqttServer.once('error', reject);
88
84
  });
89
85
  } catch (err) {
90
86
  this.adapter.log.error(`createMQTTServer error: ${err && err.message ? err.message : String(err)}`);
@@ -102,22 +98,24 @@ class MqttServerController {
102
98
  return;
103
99
  }
104
100
  try {
105
- this.aedesBroker = await Aedes.createBroker();
106
- this.mqttServer = net.createServer(this.aedesBroker.handle);
101
+ const aedes = Aedes();
102
+ this.aedesBroker = aedes;
103
+ this.mqttServer = net.createServer(aedes.handle);
107
104
  this._attachServerEvents('DummyMQTT-Server');
108
105
 
109
106
  await new Promise((resolve, reject) => {
107
+ this.mqttServer.once('error', reject);
110
108
  this.mqttServer.listen(
111
109
  this.adapter.config.mqttServerPort,
112
110
  this.adapter.config.mqttServerIPBind,
113
111
  () => {
112
+ this.mqttServer.removeListener('error', reject);
114
113
  this.adapter.log.info(
115
114
  `Starting DummyMQTT-Server on IP ${this.adapter.config.mqttServerIPBind} and Port ${this.adapter.config.mqttServerPort}`
116
115
  );
117
116
  resolve();
118
117
  }
119
118
  );
120
- this.mqttServer.once('error', reject);
121
119
  });
122
120
  } catch (err) {
123
121
  this.adapter.log.error(`createDummyMQTTServer error: ${err && err.message ? err.message : String(err)}`);
@@ -130,6 +128,17 @@ class MqttServerController {
130
128
  * Wird beim Adapter-Stop oder nach einem Fehler beim Start aufgerufen.
131
129
  */
132
130
  closeServer() {
131
+ if (this.aedesBroker) {
132
+ const broker = this.aedesBroker;
133
+ this.aedesBroker = null;
134
+ try {
135
+ broker.close(() => {
136
+ this.adapter.log.debug('Aedes broker closed.');
137
+ });
138
+ } catch (err) {
139
+ this.adapter.log.error(`closeServer aedesBroker.close() error: ${err}`);
140
+ }
141
+ }
133
142
  if (this.mqttServer) {
134
143
  const server = this.mqttServer;
135
144
  this.mqttServer = null;
@@ -143,21 +152,6 @@ class MqttServerController {
143
152
  this.adapter.log.error(`closeServer mqttServer.close() error: ${err}`);
144
153
  }
145
154
  }
146
- if (this.aedesBroker) {
147
- const broker = this.aedesBroker;
148
- this.aedesBroker = null;
149
- try {
150
- broker.close((err) => {
151
- if (err) {
152
- this.adapter.log.debug(`Aedes broker close callback error: ${err}`);
153
- } else {
154
- this.adapter.log.debug('Aedes broker closed.');
155
- }
156
- });
157
- } catch (err) {
158
- this.adapter.log.error(`closeServer aedesBroker.close() error: ${err}`);
159
- }
160
- }
161
155
  }
162
156
  }
163
157
 
package/lib/states.js CHANGED
@@ -347,12 +347,19 @@ const states = {
347
347
  },
348
348
 
349
349
  /**
350
- * Illuminance berechnet aus dem Z2M-Rohwert (prop='illuminance').
351
- * Formel: lux = round(10^(raw / 10000))
352
- * Wird zusätzlich zu illuminance_raw angelegt wenn das Gerät keinen
353
- * illuminance_lux-Expose liefert.
350
+ * illuminance (Lux-State) ausgelöst durch payload.illuminance.
351
+ *
352
+ * Fall 1 beide Werte im Payload (illuminance + illuminance_raw):
353
+ * payload.illuminance wird direkt übernommen (neues Z2M sendet bereits Lux).
354
+ *
355
+ * Fall 2 – nur illuminance im Payload:
356
+ * payload.illuminance wird direkt übernommen.
357
+ *
358
+ * Fall 3 – nur illuminance_raw im Payload:
359
+ * Dieser State wird NICHT von diesem Eintrag gesetzt (prop='illuminance' nicht vorhanden).
360
+ * Stattdessen übernimmt illuminance_from_raw (prop='illuminance_raw') die Berechnung.
354
361
  */
355
- illuminance_from_raw: {
362
+ illuminance_direct: {
356
363
  id: 'illuminance',
357
364
  prop: 'illuminance',
358
365
  name: 'Illuminance',
@@ -363,13 +370,20 @@ const states = {
363
370
  type: 'number',
364
371
  def: 0,
365
372
  unit: 'lux',
366
- getter: (payload) => {
367
- if (payload.illuminance == null) { return undefined; }
368
- return Math.round(Math.pow(10, payload.illuminance / 10000));
369
- },
370
373
  },
371
374
 
372
- illuminance_raw: {
375
+ /**
376
+ * illuminance_raw (Rohwert-State) – ausgelöst durch payload.illuminance.
377
+ *
378
+ * Fall 2 – nur illuminance im Payload:
379
+ * illuminance_raw wird aus dem Lux-Wert zurückgerechnet:
380
+ * raw = round(log10(lux) * 10000 + 1)
381
+ *
382
+ * Fall 1 – beide Werte im Payload:
383
+ * Getter gibt undefined zurück → dieser State wird NICHT gesetzt,
384
+ * stattdessen setzt illuminance_raw_direct (prop='illuminance_raw') den Rohwert direkt.
385
+ */
386
+ illuminance_raw_from_lux: {
373
387
  id: 'illuminance_raw',
374
388
  prop: 'illuminance',
375
389
  name: 'Illuminance raw',
@@ -380,11 +394,21 @@ const states = {
380
394
  type: 'number',
381
395
  def: 0,
382
396
  unit: '',
397
+ getter: (payload) => {
398
+ // Wenn illuminance_raw bereits im Payload → illuminance_raw_direct setzt ihn direkt
399
+ if (payload.illuminance_raw != null) { return undefined; }
400
+ const lux = payload.illuminance;
401
+ if (lux == null) { return undefined; }
402
+ if (lux <= 0) { return 0; }
403
+ return Math.round(Math.log10(lux) * 10000 + 1);
404
+ },
383
405
  },
384
406
 
385
407
  /**
386
- * Rohwert-State wenn das Gerät 'illuminance_raw' als payload-Schlüssel sendet
387
- * (z.B. Geräte mit eigenem illuminance_raw-Expose).
408
+ * illuminance_raw (Rohwert-State) ausgelöst durch payload.illuminance_raw.
409
+ *
410
+ * Fall 1 – beide Werte im Payload: direkt übernehmen.
411
+ * Fall 3 – nur illuminance_raw im Payload: direkt übernehmen.
388
412
  */
389
413
  illuminance_raw_direct: {
390
414
  id: 'illuminance_raw',
@@ -400,12 +424,15 @@ const states = {
400
424
  },
401
425
 
402
426
  /**
403
- * Illuminance in Lux berechnet aus payload.illuminance_raw.
404
- * Formel: lux = round(10^(illuminance_raw / 10000))
405
- * Wird angelegt wenn das Gerät 'illuminance_raw' als Expose-Typ hat,
406
- * aber kein separates 'illuminance_lux' liefert.
427
+ * illuminance (Lux-State) ausgelöst durch payload.illuminance_raw.
428
+ *
429
+ * Fall 3 nur illuminance_raw im Payload:
430
+ * Lux wird berechnet: lux = round(10^((raw - 1) / 10000))
431
+ *
432
+ * Fall 1 – beide Werte im Payload:
433
+ * Getter gibt undefined zurück → illuminance_direct (prop='illuminance') setzt Lux direkt.
407
434
  */
408
- illuminance_from_illuminance_raw: {
435
+ illuminance_from_raw: {
409
436
  id: 'illuminance',
410
437
  prop: 'illuminance_raw',
411
438
  name: 'Illuminance',
@@ -417,8 +444,55 @@ const states = {
417
444
  def: 0,
418
445
  unit: 'lux',
419
446
  getter: (payload) => {
420
- if (payload.illuminance_raw == null) { return undefined; }
421
- return Math.round(Math.pow(10, payload.illuminance_raw / 10000));
447
+ // Wenn illuminance bereits im Payload illuminance_direct setzt Lux direkt
448
+ if (payload.illuminance != null) { return undefined; }
449
+ const raw = payload.illuminance_raw;
450
+ if (raw == null) { return undefined; }
451
+ if (raw <= 0) { return 0; }
452
+ return Math.round(Math.pow(10, (raw - 1) / 10000));
453
+ },
454
+ },
455
+
456
+ /**
457
+ * illuminance (Lux-State) – ausgelöst durch payload.illuminance_lux.
458
+ * Geräte wie Philips HUE Motion, IKEA liefern illuminance_lux (bereits Lux).
459
+ * Wert wird direkt übernommen.
460
+ */
461
+ illuminance_lux_direct: {
462
+ id: 'illuminance',
463
+ prop: 'illuminance_lux',
464
+ name: 'Illuminance',
465
+ icon: undefined,
466
+ role: 'value.brightness',
467
+ write: false,
468
+ read: true,
469
+ type: 'number',
470
+ def: 0,
471
+ unit: 'lux',
472
+ },
473
+
474
+ /**
475
+ * illuminance_raw (Rohwert-State) – ausgelöst durch payload.illuminance_lux.
476
+ * Berechnet: raw = round(log10(lux) * 10000 + 1)
477
+ * NUR wenn kein illuminance_raw im Payload – kein Überschreiben.
478
+ */
479
+ illuminance_raw_from_lux2: {
480
+ id: 'illuminance_raw',
481
+ prop: 'illuminance_lux',
482
+ name: 'Illuminance raw',
483
+ icon: undefined,
484
+ role: 'value.brightness',
485
+ write: false,
486
+ read: true,
487
+ type: 'number',
488
+ def: 0,
489
+ unit: '',
490
+ getter: (payload) => {
491
+ if (payload.illuminance_raw != null) { return undefined; }
492
+ const lux = payload.illuminance_lux;
493
+ if (lux == null) { return undefined; }
494
+ if (lux <= 0) { return 0; }
495
+ return Math.round(Math.log10(lux) * 10000 + 1);
422
496
  },
423
497
  },
424
498
 
@@ -157,7 +157,7 @@ class StatesController {
157
157
  if (typeof value === 'object') {
158
158
  value = JSON.stringify(value);
159
159
  }
160
- await this.adapter.setStateAsync(`${fullPath}.${key}`, value, true);
160
+ this.adapter.setState(`${fullPath}.${key}`, value, true);
161
161
  }
162
162
  continue;
163
163
  }
@@ -233,7 +233,7 @@ class StatesController {
233
233
  if (value === undefined || value === null) {
234
234
  return;
235
235
  }
236
- await this.adapter.setStateAsync(stateName, value, true);
236
+ this.adapter.setState(stateName, value, true);
237
237
  }
238
238
 
239
239
  /**
@@ -264,7 +264,7 @@ class StatesController {
264
264
  return;
265
265
  }
266
266
 
267
- await this.adapter.setStateAsync(stateName, value, true);
267
+ this.adapter.setState(stateName, value, true);
268
268
 
269
269
  // Auto-Reset (false → nichts tun, true → nach timeout zurücksetzen)
270
270
  // Wenn value=false (z.B. Stop-Aktion im simpleMoveStopState-Modus),
@@ -276,7 +276,7 @@ class StatesController {
276
276
  if (value === true) {
277
277
  this.timeOutCache[stateName] = this.adapter.setTimeout(() => {
278
278
  delete this.timeOutCache[stateName];
279
- this.adapter.setStateAsync(stateName, false, true).catch((err) => {
279
+ this.adapter.setState(stateName, false, true).catch((err) => {
280
280
  this.adapter.log.debug(`setStateWithTimeout reset error for ${stateName}: ${err}`);
281
281
  });
282
282
  }, timeout);
@@ -133,8 +133,8 @@ class WebsocketController {
133
133
  this.adapter.log.debug(`WebSocket closed – code: ${code}, reason: ${reason ? reason.toString() : 'none'}`);
134
134
 
135
135
  try {
136
+ await this.adapter.setStateChangedAsync('info.connection', false, true);
136
137
  if (this.adapter.statesController) {
137
- this.adapter.setStateChanged('info.connection', false, true);
138
138
  await this.adapter.statesController.setAllAvailableToFalse();
139
139
  }
140
140
  } catch (err) {
@@ -87,7 +87,7 @@ class Z2mController {
87
87
  if (stateID === 'send_payload') {
88
88
  try {
89
89
  controlObj.payload = JSON.parse(stateVal);
90
- await this.adapter.setStateAsync(id, state.val, true);
90
+ this.adapter.setState(id, state.val, true);
91
91
  } catch (error) {
92
92
  // Fix 9: rawStr als String direkt als payload – wird in main.js via
93
93
  // JSON.stringify() nochmals gewrappt → würde doppelt quoten.
@@ -122,7 +122,7 @@ class Z2mController {
122
122
  if (deviceState.isOption) {
123
123
  // set optionsValues 'Cache'
124
124
  device.optionsValues[stateName] = state.val;
125
- await this.adapter.setStateAsync(id, state.val, true);
125
+ this.adapter.setState(id, state.val, true);
126
126
  return;
127
127
  }
128
128
 
@@ -130,7 +130,7 @@ class Z2mController {
130
130
  const immediateAckRole = ['button'].includes(deviceState.role);
131
131
  const immediateAckId = ['brightness_move', 'colortemp_move', 'brightness_step', 'effect'].includes(deviceState.id);
132
132
  if (immediateAckRole || immediateAckId) {
133
- await this.adapter.setStateAsync(id, state.val, true);
133
+ this.adapter.setState(id, state.val, true);
134
134
  }
135
135
 
136
136
  if (this.logCustomizations.debugDevices) {
package/main.js CHANGED
@@ -71,7 +71,7 @@ class Zigbee2mqtt extends core.Adapter {
71
71
 
72
72
  adapterInfo(this.config, this.log);
73
73
 
74
- await this.setStateAsync('info.connection', false, true);
74
+ this.setState('info.connection', false, true);
75
75
 
76
76
  const debugDevicesState = await this.getStateAsync('info.debugmessages');
77
77
  if (debugDevicesState && debugDevicesState.val) {
@@ -109,6 +109,7 @@ class Zigbee2mqtt extends core.Adapter {
109
109
  clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
110
110
  clean: true,
111
111
  reconnectPeriod: 500,
112
+ connectTimeout: 10000,
112
113
  };
113
114
 
114
115
  if (this.config.externalMqttServerCredentials === true) {
@@ -130,6 +131,7 @@ class Zigbee2mqtt extends core.Adapter {
130
131
  clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
131
132
  clean: true,
132
133
  reconnectPeriod: 500,
134
+ connectTimeout: 10000,
133
135
  });
134
136
  }
135
137
 
@@ -156,18 +158,25 @@ class Zigbee2mqtt extends core.Adapter {
156
158
  this.log.info('MQTT client reconnecting to Zigbee2MQTT...');
157
159
  });
158
160
 
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}`);
161
+ this.mqttClient.on('offline', async () => {
162
+ this.log.warn('MQTT client offline – connection to Zigbee2MQTT lost.');
163
+ await this.setStateChangedAsync('info.connection', false, true).catch((e) =>
164
+ this.log.error(`MQTT offline setStateChangedAsync error: ${e}`)
165
+ );
166
+ try {
167
+ if (this.statesController) {
168
+ await this.statesController.setAllAvailableToFalse();
169
169
  }
170
- })().catch((e) => this.log.error(`MQTT offline handler error: ${e}`));
170
+ } catch (e) {
171
+ this.log.error(`MQTT offline setAllAvailableToFalse error: ${e}`);
172
+ }
173
+ });
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
+ );
171
180
  });
172
181
 
173
182
  this.mqttClient.on('error', (err) => {
@@ -513,7 +522,7 @@ class Zigbee2mqtt extends core.Adapter {
513
522
  async onUnload(callback) {
514
523
  try {
515
524
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
516
- if (this.mqttClient && !this.mqttClient.disconnected) {
525
+ if (this.mqttClient && this.mqttClient.connected) {
517
526
  try {
518
527
  this.mqttClient.removeAllListeners();
519
528
  this.mqttClient.end(true);
@@ -561,7 +570,7 @@ class Zigbee2mqtt extends core.Adapter {
561
570
  this.log.error(e);
562
571
  }
563
572
 
564
- await this.setStateAsync('info.connection', false, true);
573
+ this.setState('info.connection', false, true);
565
574
 
566
575
  // Schedule-Job beenden
567
576
  const job = schedule.scheduledJobs['coordinatorCheck'];
@@ -586,14 +595,14 @@ class Zigbee2mqtt extends core.Adapter {
586
595
  if (state && state.ack === false) {
587
596
  if (id.endsWith('info.debugmessages')) {
588
597
  this.logCustomizations.debugDevices = state.val != null ? String(state.val) : '';
589
- await this.setStateAsync(id, state.val, true);
598
+ this.setState(id, state.val, true);
590
599
  return;
591
600
  }
592
601
  if (id.endsWith('info.logfilter')) {
593
602
  this.logCustomizations.logfilter = state.val != null
594
603
  ? String(state.val).split(';').filter((x) => x)
595
604
  : [];
596
- await this.setStateAsync(id, state.val, true);
605
+ this.setState(id, state.val, true);
597
606
  return;
598
607
  }
599
608
 
@@ -608,7 +617,7 @@ class Zigbee2mqtt extends core.Adapter {
608
617
  }
609
618
 
610
619
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
611
- if (!this.mqttClient || this.mqttClient.disconnected) {
620
+ if (!this.mqttClient || !this.mqttClient.connected) {
612
621
  this.log.warn(`Cannot publish state, MQTT client not connected. (${id})`);
613
622
  return;
614
623
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.zigbee2mqtt",
3
- "version": "3.1.5",
3
+ "version": "3.2.1",
4
4
  "description": "Zigbee2MQTT adapter for ioBroker",
5
5
  "author": {
6
6
  "name": "Dennis Rathjen and Arthur Rupp",
@@ -21,15 +21,15 @@
21
21
  "url": "https://github.com/arteck/ioBroker.zigbee2mqtt.git"
22
22
  },
23
23
  "engines": {
24
- "node": ">= 20"
24
+ "node": ">= 22"
25
25
  },
26
26
  "dependencies": {
27
27
  "@iobroker/adapter-core": "^3.3.2",
28
28
  "@iobroker/dm-utils": "^3.0.17",
29
- "aedes": "^1.0.2",
30
- "aedes-persistence-nedb": "^2.0.3",
29
+ "aedes": "^0.51.3",
30
+ "aedes-persistence": "^9.1.2",
31
+ "axios": "^1.15.2",
31
32
  "mqtt": "^5.15.1",
32
- "axios": "^1.14.0",
33
33
  "net": "^1.0.2",
34
34
  "node-schedule": "^2.1.1",
35
35
  "sharp": "^0.34.5",
@@ -41,12 +41,12 @@
41
41
  "@alcalzone/release-script-plugin-license": "^5.1.1",
42
42
  "@alcalzone/release-script-plugin-manual-review": "^5.1.1",
43
43
  "@iobroker/adapter-dev": "^1.5.0",
44
- "@iobroker/testing": "^5.2.2",
45
44
  "@iobroker/eslint-config": "^2.1.0",
45
+ "@iobroker/testing": "^5.2.2",
46
46
  "@tsconfig/node14": "^14.1.8",
47
47
  "@types/node": "^25.5.2",
48
48
  "@types/node-schedule": "^2.1.8",
49
- "typescript": "~6.0.2"
49
+ "typescript": "~6.0.3"
50
50
  },
51
51
  "main": "main.js",
52
52
  "files": [