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/main.js CHANGED
@@ -1,7 +1,4 @@
1
1
  'use strict';
2
- /*
3
- * Created with @iobroker/create-adapter v2.2.1
4
- */
5
2
 
6
3
  // The adapter-core module gives you access to the core ioBroker functions
7
4
  // you need to create an adapter
@@ -18,134 +15,174 @@ const StatesController = require('./lib/statesController').StatesController;
18
15
  const WebsocketController = require('./lib/websocketController').WebsocketController;
19
16
  const MqttServerController = require('./lib/mqttServerController').MqttServerController;
20
17
 
21
- let mqttClient;
22
-
23
- let deviceCache = [];
24
-
25
- let groupCache = [];
26
- const createCache = {};
27
- const logCustomizations = { debugDevices: '', logfilter: [] };
28
- let showInfo = true;
29
- let statesController;
30
- let deviceController;
31
- let z2mController;
32
- let websocketController;
33
- let mqttServerController;
34
-
35
- let messageParseMutex = Promise.resolve();
36
-
37
18
  class Zigbee2mqtt extends core.Adapter {
38
19
  constructor(options) {
39
20
  super({
40
21
  ...options,
41
22
  name: 'zigbee2mqtt',
42
23
  });
24
+
25
+ // Instance-level state (kein Modul-globaler Zustand)
26
+ this.mqttClient = null;
27
+ this.deviceCache = [];
28
+ this.groupCache = [];
29
+ this.createCache = {};
30
+ this.logCustomizations = { debugDevices: '', logfilter: [] };
31
+ this.showInfo = true;
32
+ this.statesController = null;
33
+ this.deviceController = null;
34
+ this.z2mController = null;
35
+ this.websocketController = null;
36
+ this.mqttServerController = null;
37
+ this.messageParseMutex = Promise.resolve();
38
+
43
39
  this.on('ready', this.onReady.bind(this));
44
40
  this.on('stateChange', this.onStateChange.bind(this));
41
+ this.on('message', this.onMessage.bind(this));
45
42
  this.on('unload', this.onUnload.bind(this));
46
43
  }
47
44
 
48
45
  async onReady() {
49
- statesController = new StatesController(this, deviceCache, groupCache, logCustomizations, createCache);
50
- deviceController = new DeviceController(
46
+ this.statesController = new StatesController(this, this.deviceCache, this.groupCache, this.logCustomizations, this.createCache);
47
+ this.deviceController = new DeviceController(
51
48
  this,
52
- deviceCache,
53
- groupCache,
49
+ this.deviceCache,
50
+ this.groupCache,
54
51
  this.config,
55
- logCustomizations,
56
- createCache
52
+ this.logCustomizations,
53
+ this.createCache
57
54
  );
58
- z2mController = new Z2mController(this, deviceCache, groupCache, logCustomizations);
55
+ this.z2mController = new Z2mController(this, this.deviceCache, this.groupCache, this.logCustomizations);
59
56
 
60
- // Initialize your adapter here
61
- adapterInfo(this.config, this.log);
57
+ // Fix 2: adapterInfo ist async → awaiten
58
+ await adapterInfo(this.config, this.log);
62
59
 
63
60
  this.setState('info.connection', false, true);
64
61
 
65
62
  const debugDevicesState = await this.getStateAsync('info.debugmessages');
66
63
  if (debugDevicesState && debugDevicesState.val) {
67
- logCustomizations.debugDevices = String(debugDevicesState.val);
64
+ this.logCustomizations.debugDevices = String(debugDevicesState.val);
68
65
  }
69
66
 
70
67
  const logfilterState = await this.getStateAsync('info.logfilter');
71
- if (logfilterState && logfilterState.val) {
72
- logCustomizations.logfilter = String(logfilterState.val)
68
+ if (logfilterState && logfilterState.val) {
69
+ this.logCustomizations.logfilter = String(logfilterState.val)
73
70
  .split(';')
74
- .filter((x) => x); // filter removes empty strings here
71
+ .filter((x) => x);
75
72
  }
76
73
 
77
- if (this.config.coordinatorCheck == true) {
74
+ if (this.config.coordinatorCheck === true) {
78
75
  try {
79
- schedule.scheduleJob('coordinatorCheck', this.config.coordinatorCheckCron, () =>
80
- this.onStateChange('manual_trigger._.info.coordinator_check', { ack: false })
81
- );
76
+ schedule.scheduleJob('coordinatorCheck', this.config.coordinatorCheckCron, () => {
77
+ this.onStateChange('manual_trigger._.info.coordinator_check', { ack: false, val: null })
78
+ .catch((e) => this.log.error(`coordinatorCheck trigger error: ${e}`));
79
+ });
82
80
  } catch (e) {
83
81
  this.log.error(e);
84
82
  }
85
83
  }
84
+
86
85
  // MQTT
87
86
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
88
87
  // External MQTT-Server
89
- if (this.config.connectionType == 'exmqtt') {
90
- if (this.config.externalMqttServerIP == '') {
88
+ if (this.config.connectionType === 'exmqtt') {
89
+ if (!this.config.externalMqttServerIP) {
91
90
  this.log.warn('Please configure the External MQTT-Server connection!');
92
91
  return;
93
92
  }
94
93
 
95
- // MQTT connection settings
96
94
  const mqttClientOptions = {
97
95
  clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
98
96
  clean: true,
99
97
  reconnectPeriod: 500,
100
98
  };
101
99
 
102
- // Set external mqtt credentials
103
- if (this.config.externalMqttServerCredentials == true) {
100
+ if (this.config.externalMqttServerCredentials === true) {
104
101
  mqttClientOptions.username = this.config.externalMqttServerUsername;
105
102
  mqttClientOptions.password = this.config.externalMqttServerPassword;
106
103
  }
107
104
 
108
- // Init connection
109
- mqttClient = mqtt.connect(
105
+ this.mqttClient = mqtt.connect(
110
106
  `mqtt://${this.config.externalMqttServerIP}:${this.config.externalMqttServerPort}`,
111
107
  mqttClientOptions
112
108
  );
113
109
  } else {
114
- // Internal MQTT-Server
115
- mqttServerController = new MqttServerController(this);
116
- await mqttServerController.createMQTTServer();
110
+ // Internal MQTT-Server
111
+ this.mqttServerController = new MqttServerController(this);
112
+ await this.mqttServerController.createMQTTServer();
117
113
  await this.delay(1500);
118
- mqttClient = mqtt.connect(`mqtt://${this.config.mqttServerIPBind}:${this.config.mqttServerPort}`, {
114
+ this.mqttClient = mqtt.connect(`mqtt://${this.config.mqttServerIPBind}:${this.config.mqttServerPort}`, {
119
115
  clientId: `ioBroker.zigbee2mqtt_${Math.random().toString(16).slice(2, 8)}`,
120
116
  clean: true,
121
117
  reconnectPeriod: 500,
122
118
  });
123
119
  }
124
120
 
125
- // MQTT Client
126
- mqttClient.on('connect', () => {
121
+ // MQTT Client Events
122
+ this.mqttClient.on('connect', () => {
127
123
  this.log.info(
128
- `Connect to Zigbee2MQTT over ${this.config.connectionType == 'exmqtt' ? 'external mqtt' : 'internal mqtt'} connection.`
124
+ `Connect to Zigbee2MQTT over ${this.config.connectionType === 'exmqtt' ? 'external mqtt' : 'internal mqtt'} connection.`
129
125
  );
126
+ this.setStateChanged('info.connection', true, true);
127
+ this.mqttClient.subscribe(`${this.config.baseTopic}/#`, (err) => {
128
+ if (err) {
129
+ this.log.error(`MQTT subscribe error: ${err && err.message ? err.message : String(err)}`);
130
+ }
131
+ });
130
132
  });
131
133
 
132
- mqttClient.subscribe(`${this.config.baseTopic}/#`);
134
+ this.mqttClient.on('reconnect', () => {
135
+ this.log.info('MQTT client reconnecting to Zigbee2MQTT...');
136
+ });
133
137
 
134
- mqttClient.on('message', (topic, payload) => {
135
- const newMessage = `{"payload":${payload.toString() == '' ? '"null"' : payload.toString()},"topic":"${topic.slice(topic.search('/') + 1)}"}`;
136
- this.messageParse(newMessage);
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();
144
+ }
145
+ } catch (e) {
146
+ this.log.error(`MQTT offline setAllAvailableToFalse error: ${e}`);
147
+ }
137
148
  });
138
- } else if (this.config.connectionType == 'ws') {
139
- // Websocket
140
- if (this.config.wsServerIP == '') {
149
+
150
+ this.mqttClient.on('error', (err) => {
151
+ this.log.error(`MQTT client error: ${err && err.message ? err.message : String(err)}`);
152
+ });
153
+
154
+ 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) {
158
+ this.log.debug(`MQTT message with unexpected topic format: ${topic}`);
159
+ return;
160
+ }
161
+ const payloadStr = payload.toString();
162
+ let parsedPayload;
163
+ try {
164
+ parsedPayload = payloadStr === '' ? null : JSON.parse(payloadStr);
165
+ } catch {
166
+ parsedPayload = payloadStr;
167
+ }
168
+ const messageObj = {
169
+ payload: parsedPayload,
170
+ topic: topic.slice(sepIdx + 1),
171
+ };
172
+ this.messageParse(messageObj).catch((err) => {
173
+ this.log.error(`messageParse error: ${err}`);
174
+ });
175
+ });
176
+ } else if (this.config.connectionType === 'ws') {
177
+ // Websocket
178
+ if (!this.config.wsServerIP) {
141
179
  this.log.warn('Please configure the Websoket connection!');
142
180
  return;
143
181
  }
144
182
 
145
- // Dummy MQTT-Server
146
- if (this.config.dummyMqtt == true) {
147
- mqttServerController = new MqttServerController(this);
148
- await mqttServerController.createDummyMQTTServer();
183
+ if (this.config.dummyMqtt === true) {
184
+ this.mqttServerController = new MqttServerController(this);
185
+ await this.mqttServerController.createDummyMQTTServer();
149
186
  await this.delay(1500);
150
187
  }
151
188
 
@@ -154,231 +191,390 @@ class Zigbee2mqtt extends core.Adapter {
154
191
  }
155
192
 
156
193
  startWebsocket() {
157
- websocketController = new WebsocketController(this);
158
- const wsClient = websocketController.initWsClient();
159
-
160
- if (wsClient) {
161
- wsClient.on('open', () => {
162
- this.log.info('Connect to Zigbee2MQTT over websocket connection.');
163
- });
164
-
165
- wsClient.on('message', (message) => {
166
- this.messageParse(message);
167
- });
168
-
169
- wsClient.on('close', async () => {
170
- this.setStateChanged('info.connection', false, true);
171
- await statesController.setAllAvailableToFalse();
172
- deviceCache = [];
173
- groupCache = [];
174
- });
194
+ // Controller nur einmal erstellen – bei Reconnect wird derselbe wiederverwendet
195
+ if (!this.websocketController) {
196
+ this.websocketController = new WebsocketController(this);
175
197
  }
198
+ this.websocketController.initWsClient();
176
199
  }
177
200
 
178
- async messageParse(message) {
179
- // Mutex lock: queue up calls to messageParse
180
- let release;
201
+ async messageParse(messageObj) {
202
+ // Mutex: serialisiert alle messageParse-Aufrufe
203
+ // Wichtig: lock muss IMMER wieder freigegeben werden, auch bei early return / Fehler.
204
+ let release = () => {};
181
205
  const lock = new Promise((resolve) => (release = resolve));
182
- const prev = messageParseMutex;
183
- messageParseMutex = lock;
184
- await prev;
206
+ const prev = this.messageParseMutex;
207
+ this.messageParseMutex = lock;
208
+
209
+ // Wenn prev rejected hat, wollen wir trotzdem weiterarbeiten (sonst blockiert alles)
185
210
  try {
186
- // If the MQTT output type is set to attribute_and_json, the non-valid JSON must be checked here.
187
- if (utils.isJson(message) == false) {
211
+ await prev;
212
+ } catch {
213
+ // ignore
214
+ }
215
+
216
+ try {
217
+ if (!messageObj || typeof messageObj !== 'object') {
218
+ return;
219
+ }
220
+ if (!this.statesController || !this.deviceController || !this.z2mController) {
221
+ this.log.debug('messageParse: controllers not yet initialized, dropping message.');
188
222
  return;
189
223
  }
190
-
191
- const messageObj = JSON.parse(message);
192
224
 
193
225
  switch (messageObj.topic) {
194
- case 'Coordinator/availability':
195
- this.setStateChanged('info.coordinator_status', messageObj.payload.state, true);
226
+ case 'Coordinator/availability': {
227
+ const coordState = typeof messageObj.payload === 'object' && messageObj.payload !== null
228
+ ? messageObj.payload.state
229
+ : messageObj.payload;
230
+ this.setStateChanged('info.coordinator_status', coordState, true);
196
231
  break;
232
+ }
197
233
  case 'bridge/info':
198
- if (showInfo) {
199
- zigbee2mqttInfo(messageObj.payload, this.log);
200
- checkConfig(messageObj.payload.config, this.log, messageObj.payload.version);
201
- showInfo = false;
234
+ if (this.showInfo && messageObj.payload) {
235
+ await zigbee2mqttInfo(messageObj.payload, this.log);
236
+ if (messageObj.payload.config && messageObj.payload.version) {
237
+ checkConfig(messageObj.payload.config, this.log, messageObj.payload.version);
238
+ }
239
+ this.showInfo = false;
202
240
  }
203
241
  break;
204
- case 'bridge/state':
205
- if (messageObj.payload.state != 'online') {
206
- statesController.setAllAvailableToFalse();
242
+ case 'bridge/state': {
243
+ const bridgeState = typeof messageObj.payload === 'object' && messageObj.payload !== null
244
+ ? messageObj.payload.state
245
+ : messageObj.payload;
246
+ if (bridgeState !== 'online') {
247
+ await this.statesController.setAllAvailableToFalse();
248
+ this.showInfo = true;
207
249
  }
208
- this.setStateChanged('info.connection', messageObj.payload.state == 'online', true);
250
+ this.setStateChanged('info.connection', bridgeState === 'online', true);
209
251
  break;
252
+ }
210
253
  case 'bridge/devices':
211
- await deviceController.createDeviceDefinitions(messageObj.payload);
212
- await deviceController.createOrUpdateDevices();
213
- await deviceController.checkAndProgressDeviceRemove();
214
- await statesController.subscribeWritableStates();
215
- statesController.processQueue();
254
+ await this.deviceController.createDeviceDefinitions(messageObj.payload);
255
+ await this.deviceController.createOrUpdateDevices();
256
+ await this.deviceController.checkAndProgressDeviceRemove();
257
+ await this.statesController.subscribeWritableStates();
258
+ await this.statesController.processQueue();
216
259
  break;
217
260
  case 'bridge/groups':
218
- await deviceController.createGroupDefinitions(messageObj.payload);
219
- await deviceController.createOrUpdateDevices();
220
- await statesController.subscribeWritableStates();
221
- statesController.processQueue();
261
+ await this.deviceController.createGroupDefinitions(messageObj.payload);
262
+ await this.deviceController.createOrUpdateDevices();
263
+ await this.statesController.subscribeWritableStates();
264
+ await this.statesController.processQueue();
222
265
  break;
223
266
  case 'bridge/response/coordinator_check':
224
- deviceController.processCoordinatorCheck(messageObj.payload);
267
+ await this.deviceController.processCoordinatorCheck(messageObj.payload);
225
268
  break;
226
269
  case 'bridge/logging':
227
- if (this.config.proxyZ2MLogs == true) {
228
- z2mController.proxyZ2MLogs(messageObj);
270
+ if (this.config.proxyZ2MLogs === true) {
271
+ await this.z2mController.proxyZ2MLogs(messageObj);
229
272
  }
230
273
  break;
231
274
  case 'bridge/response/device/rename':
232
- await deviceController.renameDeviceInCache(messageObj);
233
- await deviceController.createOrUpdateDevices();
234
- statesController.processQueue();
275
+ await this.deviceController.renameDeviceInCache(messageObj);
276
+ await this.deviceController.createOrUpdateDevices();
277
+ await this.statesController.processQueue();
235
278
  break;
279
+ case 'bridge/event': {
280
+ const evType = messageObj.payload && messageObj.payload.type;
281
+ const evData = messageObj.payload && messageObj.payload.data;
282
+ if (evType === 'device_announce' && evData && evData.friendly_name) {
283
+ const newMessage = { payload: { available: true }, topic: evData.friendly_name };
284
+ await this.statesController.processDeviceMessage(newMessage);
285
+ } else if (evType === 'device_leave' && evData && evData.friendly_name) {
286
+ const newMessage = { payload: { available: false }, topic: evData.friendly_name };
287
+ await this.statesController.processDeviceMessage(newMessage);
288
+ }
289
+ break;
290
+ }
236
291
  case 'bridge/config':
237
292
  case 'bridge/health':
238
293
  case 'bridge/definitions':
239
- case 'bridge/event':
240
294
  case 'bridge/extensions':
241
295
  case 'bridge/response/device/configure':
242
296
  case 'bridge/response/device/remove':
243
297
  case 'bridge/response/device/options':
298
+ case 'bridge/response/device/interview':
244
299
  case 'bridge/response/permit_join':
245
300
  case 'bridge/response/networkmap':
301
+ case 'bridge/response/options':
302
+ case 'bridge/response/restart':
303
+ case 'bridge/response/backup':
304
+ case 'bridge/response/install_code/add':
305
+ case 'bridge/response/group/add':
306
+ case 'bridge/response/group/remove':
307
+ case 'bridge/response/group/members/add':
308
+ case 'bridge/response/group/members/remove':
246
309
  case 'bridge/response/touchlink/scan':
247
310
  case 'bridge/response/touchlink/identify':
248
311
  case 'bridge/response/touchlink/factory_reset':
249
312
  break;
250
- default:
251
- {
252
- // is the payload an availability status?
253
- if (messageObj.topic.endsWith('/availability')) {
254
- // If an availability message for an old device ID comes with a payload of NULL, this is the indicator that a device has been unnamed.
255
- if (messageObj.payload == 'null') {
256
- return;
257
- }
258
- // is it a viable payload?
259
- if (messageObj.payload && messageObj.payload.state) {
260
- // {"payload":{"state":"online"},"topic":"FL.Licht.Links/availability"} ----> {"payload":{"available":true},"topic":"FL.Licht.Links"}
261
- const newMessage = {
262
- payload: { available: messageObj.payload.state == 'online' },
263
- topic: messageObj.topic.replace('/availability', ''),
264
- };
265
-
266
- statesController.processDeviceMessage(newMessage);
267
- }
268
- // States
269
- } else {
270
- // With the MQTT output type attribute_and_json, primitive payloads arrive here that must be discarded.
271
- if (utils.isObject(messageObj.payload) == false) {
272
- return;
273
- }
274
- // If MQTT is used, I have to filter the self-sent 'set' commands.
275
- if (messageObj.topic.endsWith('/set')) {
276
- return;
277
- }
278
-
279
- statesController.processDeviceMessage(messageObj);
313
+ default: {
314
+ if (!messageObj.topic || typeof messageObj.topic !== 'string') {
315
+ break;
316
+ }
317
+ if (messageObj.topic.endsWith('/availability')) {
318
+ if (messageObj.payload === null || messageObj.payload === undefined) {
319
+ break;
320
+ }
321
+ const availState = typeof messageObj.payload === 'object'
322
+ ? messageObj.payload.state
323
+ : messageObj.payload;
324
+ const newMessage = {
325
+ payload: { available: availState === 'online' },
326
+ topic: messageObj.topic.replace('/availability', ''),
327
+ };
328
+ await this.statesController.processDeviceMessage(newMessage);
329
+ } else {
330
+ if (!utils.isObject(messageObj.payload)) {
331
+ break;
332
+ }
333
+ if (messageObj.topic.endsWith('/set')) {
334
+ break;
280
335
  }
336
+ await this.statesController.processDeviceMessage(messageObj);
281
337
  }
282
338
  break;
339
+ }
283
340
  }
284
341
  } finally {
342
+ // Mutex immer freigeben
285
343
  release();
286
344
  }
287
345
  }
288
346
 
347
+ async onMessage(obj) {
348
+ if (!obj || !obj.command) {
349
+ return;
350
+ }
351
+
352
+ if (obj.command === 'deleteOldStates') {
353
+ const deletedList = [];
354
+ const errorList = [];
355
+ try {
356
+ // Guard: Caches müssen gefüllt sein – sonst würden ALLE States gelöscht
357
+ if (this.deviceCache.length === 0 && this.groupCache.length === 0) {
358
+ const warn = 'deleteOldStates: device list is empty – adapter not connected to Zigbee2MQTT yet. Aborting.';
359
+ this.log.warn(warn);
360
+ if (obj.callback) {
361
+ this.sendTo(obj.from, obj.command, { error: warn }, obj.callback);
362
+ }
363
+ return;
364
+ }
365
+
366
+ // Nur echte Geräte (kein group_*) – Gruppen werden beim Löschen komplett ausgelassen
367
+ const knownStateIDs = new Set();
368
+ for (const device of this.deviceCache) {
369
+ if (!device || !device.ieee_address || !Array.isArray(device.states)) {
370
+ continue;
371
+ }
372
+ const base = `${this.namespace}.${device.ieee_address}`;
373
+ for (const state of device.states) {
374
+ if (state && state.id) {
375
+ knownStateIDs.add(`${base}.${state.id}`);
376
+ }
377
+ }
378
+ // additional-Channel und Pflicht-States immer erlauben
379
+ knownStateIDs.add(`${base}.additional`);
380
+ knownStateIDs.add(`${base}.available`);
381
+ knownStateIDs.add(`${base}.last_seen`);
382
+ knownStateIDs.add(`${base}.send_payload`);
383
+ }
384
+
385
+ // Alle vorhandenen Adapter-Objekte laden
386
+ const allObjects = await this.getAdapterObjectsAsync();
387
+ for (const id of Object.keys(allObjects)) {
388
+ const iobObj = allObjects[id];
389
+
390
+ // Nur States löschen – keine channels, devices, folders
391
+ if (!iobObj || iobObj.type !== 'state') {
392
+ continue;
393
+ }
394
+
395
+ // info.* States niemals löschen
396
+ if (id.includes('.info.')) {
397
+ continue;
398
+ }
399
+
400
+ // Gruppen-States (group_*) niemals löschen
401
+ // Format: adapter.instance.ieee_address.stateid → parts[2] ist ieee_address
402
+ const parts = id.split('.');
403
+ if (parts.length >= 3 && parts[2].startsWith('group_')) {
404
+ continue;
405
+ }
406
+
407
+ // additional.* Sub-States erlauben
408
+ if (parts.length >= 5) {
409
+ const additionalBase = parts.slice(0, 4).join('.');
410
+ if (knownStateIDs.has(additionalBase)) {
411
+ continue;
412
+ }
413
+ }
414
+
415
+ // Wenn nicht im bekannten Set → löschen
416
+ if (!knownStateIDs.has(id)) {
417
+ try {
418
+ await this.delObjectAsync(id);
419
+ deletedList.push(id);
420
+ this.log.debug(`deleteOldStates: deleted ${id}`);
421
+ } catch (e) {
422
+ errorList.push(id);
423
+ this.log.warn(`deleteOldStates: could not delete ${id}: ${e}`);
424
+ }
425
+ }
426
+ }
427
+
428
+ // Liste aller gelöschten States als Warning ausgeben
429
+ if (deletedList.length > 0) {
430
+ this.log.warn(`deleteOldStates: deleted ${deletedList.length} state(s):`);
431
+ for (const deletedId of deletedList) {
432
+ this.log.warn(` - ${deletedId}`);
433
+ }
434
+ }
435
+ if (errorList.length > 0) {
436
+ this.log.warn(`deleteOldStates: failed to delete ${errorList.length} state(s):`);
437
+ for (const errId of errorList) {
438
+ this.log.warn(` - ${errId}`);
439
+ }
440
+ }
441
+
442
+ const msg = `Deleted ${deletedList.length} old state(s)${errorList.length > 0 ? `, ${errorList.length} error(s)` : ''}.`;
443
+ this.log.info(`deleteOldStates: ${msg}`);
444
+ if (obj.callback) {
445
+ this.sendTo(obj.from, obj.command, { result: msg }, obj.callback);
446
+ }
447
+ } catch (e) {
448
+ this.log.error(`deleteOldStates error: ${e}`);
449
+ if (obj.callback) {
450
+ this.sendTo(obj.from, obj.command, { error: String(e) }, obj.callback);
451
+ }
452
+ }
453
+ }
454
+ }
455
+
289
456
  async onUnload(callback) {
290
- // Close MQTT connections
291
- if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
292
- if (mqttClient && !mqttClient.closed) {
457
+ try {
458
+ if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
459
+ if (this.mqttClient && !this.mqttClient.disconnected) {
460
+ try {
461
+ this.mqttClient.removeAllListeners();
462
+ this.mqttClient.end(true);
463
+ } catch (e) {
464
+ this.log.error(e);
465
+ }
466
+ }
467
+ }
468
+ if (this.config.connectionType === 'intmqtt' || this.config.dummyMqtt === true) {
469
+ try {
470
+ if (this.mqttServerController) {
471
+ this.mqttServerController.closeServer();
472
+ }
473
+ } catch (e) {
474
+ this.log.error(e);
475
+ }
476
+ } else if (this.config.connectionType === 'ws') {
293
477
  try {
294
- if (mqttClient) {
295
- mqttClient.end();
478
+ if (this.websocketController) {
479
+ this.websocketController.closeConnection();
296
480
  }
297
481
  } catch (e) {
298
482
  this.log.error(e);
299
483
  }
300
484
  }
301
- }
302
- // Internal or Dummy MQTT-Server
303
- if (this.config.connectionType == 'intmqtt' || this.config.dummyMqtt == true) {
304
485
  try {
305
- if (mqttServerController) {
306
- mqttServerController.closeServer();
486
+ if (this.statesController) {
487
+ await this.statesController.setAllAvailableToFalse();
307
488
  }
308
489
  } catch (e) {
309
490
  this.log.error(e);
310
491
  }
311
- } else if (this.config.connectionType == 'ws') {
312
- // Websocket
313
492
  try {
314
- if (websocketController) {
315
- websocketController.closeConnection();
493
+ if (this.websocketController) {
494
+ await this.websocketController.allTimerClear();
316
495
  }
317
496
  } catch (e) {
318
497
  this.log.error(e);
319
498
  }
320
- }
321
- // Set all device available states of false
322
- try {
323
- if (statesController) {
324
- await statesController.setAllAvailableToFalse();
325
- }
326
- } catch (e) {
327
- this.log.error(e);
328
- }
329
- // Clear all websocket timers
330
- try {
331
- if (websocketController) {
332
- await websocketController.allTimerClear();
333
- }
334
- } catch (e) {
335
- this.log.error(e);
336
- }
337
- // Clear all state timers
338
- try {
339
- if (statesController) {
340
- await statesController.allTimerClear();
499
+ try {
500
+ if (this.statesController) {
501
+ await this.statesController.allTimerClear();
502
+ }
503
+ } catch (e) {
504
+ this.log.error(e);
341
505
  }
342
- } catch (e) {
343
- this.log.error(e);
344
- }
345
506
 
346
- this.setState('info.connection', false, true);
507
+ this.setState('info.connection', false, true);
347
508
 
348
- callback();
509
+ // Schedule-Job beenden
510
+ const job = schedule.scheduledJobs['coordinatorCheck'];
511
+ if (job) {
512
+ job.cancel();
513
+ }
514
+ } finally {
515
+ // callback() wird IMMER aufgerufen – auch wenn oben etwas wirft
516
+ // Ohne das hängt der Adapter-Stop dauerhaft
517
+ callback();
518
+ }
349
519
  }
350
520
 
351
521
  async onStateChange(id, state) {
352
- if (state && state.ack == false) {
522
+ if (state && state.ack === false) {
353
523
  if (id.endsWith('info.debugmessages')) {
354
- logCustomizations.debugDevices = state.val;
524
+ this.logCustomizations.debugDevices = String(state.val || '');
355
525
  this.setState(id, state.val, true);
356
526
  return;
357
527
  }
358
528
  if (id.endsWith('info.logfilter')) {
359
- logCustomizations.logfilter = state.val.split(';').filter((x) => x); // filter removes empty strings here
529
+ this.logCustomizations.logfilter = String(state.val || '').split(';').filter((x) => x);
360
530
  this.setState(id, state.val, true);
361
531
  return;
362
532
  }
363
533
 
364
- const message = (await z2mController.createZ2MMessage(id, state)) || { topic: '', payload: '' };
534
+ if (!this.z2mController) {
535
+ this.log.debug(`onStateChange: z2mController not yet initialized, dropping state change for ${id}.`);
536
+ return;
537
+ }
538
+
539
+ const message = await this.z2mController.createZ2MMessage(id, state);
540
+ if (!message || !message.topic) {
541
+ return;
542
+ }
365
543
 
366
544
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
367
- mqttClient.publish(`${this.config.baseTopic}/${message.topic}`, JSON.stringify(message.payload));
368
- } else if (this.config.connectionType == 'ws') {
369
- websocketController.send(JSON.stringify(message));
545
+ if (!this.mqttClient || this.mqttClient.disconnected) {
546
+ this.log.warn(`Cannot publish state, MQTT client not connected. (${id})`);
547
+ return;
548
+ }
549
+ try {
550
+ this.mqttClient.publish(
551
+ `${this.config.baseTopic}/${message.topic}`,
552
+ JSON.stringify(message.payload),
553
+ (err) => {
554
+ if (err) {
555
+ this.log.error(`MQTT publish error for ${id}: ${err && err.message ? err.message : String(err)}`);
556
+ }
557
+ }
558
+ );
559
+ } catch (e) {
560
+ this.log.error(`MQTT publish exception for ${id}: ${e}`);
561
+ }
562
+ } else if (this.config.connectionType === 'ws') {
563
+ if (!this.websocketController) {
564
+ this.log.warn(`Cannot send state, WebSocket not initialized. (${id})`);
565
+ return;
566
+ }
567
+ this.websocketController.send(JSON.stringify({ topic: message.topic, payload: message.payload }));
370
568
  }
371
569
  }
372
570
  }
373
571
  }
374
572
 
375
573
  if (require.main !== module) {
376
- // Export the constructor in compact mode
377
574
  /**
378
575
  * @param {Partial<core.AdapterOptions>} [options]
379
576
  */
380
577
  module.exports = (options) => new Zigbee2mqtt(options);
381
578
  } else {
382
- // otherwise start the instance directly
383
579
  new Zigbee2mqtt();
384
580
  }