iobroker.zigbee2mqtt 3.0.20 → 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
+ });
132
+ });
133
+
134
+ this.mqttClient.on('reconnect', () => {
135
+ this.log.info('MQTT client reconnecting to Zigbee2MQTT...');
136
+ });
137
+
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
+ }
130
148
  });
131
149
 
132
- mqttClient.subscribe(`${this.config.baseTopic}/#`);
150
+ this.mqttClient.on('error', (err) => {
151
+ this.log.error(`MQTT client error: ${err && err.message ? err.message : String(err)}`);
152
+ });
133
153
 
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);
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
+ });
137
175
  });
138
- } else if (this.config.connectionType == 'ws') {
139
- // Websocket
140
- if (this.config.wsServerIP == '') {
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,236 +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 'bridge/config':
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);
195
231
  break;
232
+ }
196
233
  case 'bridge/info':
197
- if (showInfo) {
198
- zigbee2mqttInfo(messageObj.payload, this.log);
199
- checkConfig(messageObj.payload.config, this.log, messageObj.payload.version);
200
- 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;
201
240
  }
202
241
  break;
203
- case 'bridge/state':
204
- if (messageObj.payload.state != 'online') {
205
- 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;
206
249
  }
207
- this.setStateChanged('info.connection', messageObj.payload.state == 'online', true);
250
+ this.setStateChanged('info.connection', bridgeState === 'online', true);
208
251
  break;
252
+ }
209
253
  case 'bridge/devices':
210
- await deviceController.createDeviceDefinitions(messageObj.payload);
211
- await deviceController.createOrUpdateDevices();
212
- await deviceController.checkAndProgressDeviceRemove();
213
- await statesController.subscribeWritableStates();
214
- 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();
215
259
  break;
216
260
  case 'bridge/groups':
217
- await deviceController.createGroupDefinitions(messageObj.payload);
218
- await deviceController.createOrUpdateDevices();
219
- await statesController.subscribeWritableStates();
220
- statesController.processQueue();
221
- break;
222
- case 'bridge/event':
261
+ await this.deviceController.createGroupDefinitions(messageObj.payload);
262
+ await this.deviceController.createOrUpdateDevices();
263
+ await this.statesController.subscribeWritableStates();
264
+ await this.statesController.processQueue();
223
265
  break;
224
266
  case 'bridge/response/coordinator_check':
225
- deviceController.processCoordinatorCheck(messageObj.payload);
226
- break;
227
- case 'bridge/response/device/remove':
228
- break;
229
- case 'bridge/response/device/options':
230
- break;
231
- case 'bridge/response/permit_join':
232
- break;
233
- case 'bridge/extensions':
267
+ await this.deviceController.processCoordinatorCheck(messageObj.payload);
234
268
  break;
235
269
  case 'bridge/logging':
236
- if (this.config.proxyZ2MLogs == true) {
237
- z2mController.proxyZ2MLogs(messageObj);
270
+ if (this.config.proxyZ2MLogs === true) {
271
+ await this.z2mController.proxyZ2MLogs(messageObj);
238
272
  }
239
273
  break;
240
- case 'bridge/response/device/configure':
241
- break;
242
274
  case 'bridge/response/device/rename':
243
- await deviceController.renameDeviceInCache(messageObj);
244
- await deviceController.createOrUpdateDevices();
245
- statesController.processQueue();
275
+ await this.deviceController.renameDeviceInCache(messageObj);
276
+ await this.deviceController.createOrUpdateDevices();
277
+ await this.statesController.processQueue();
246
278
  break;
247
- case 'bridge/response/networkmap':
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
+ }
248
289
  break;
290
+ }
291
+ case 'bridge/config':
292
+ case 'bridge/health':
293
+ case 'bridge/definitions':
294
+ case 'bridge/extensions':
295
+ case 'bridge/response/device/configure':
296
+ case 'bridge/response/device/remove':
297
+ case 'bridge/response/device/options':
298
+ case 'bridge/response/device/interview':
299
+ case 'bridge/response/permit_join':
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':
249
309
  case 'bridge/response/touchlink/scan':
250
- break;
251
310
  case 'bridge/response/touchlink/identify':
252
- break;
253
311
  case 'bridge/response/touchlink/factory_reset':
254
312
  break;
255
- default:
256
- {
257
- // is the payload an availability status?
258
- if (messageObj.topic.endsWith('/availability')) {
259
- // 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.
260
- if (messageObj.payload == 'null') {
261
- return;
262
- }
263
- // is it a viable payload?
264
- if (messageObj.payload && messageObj.payload.state) {
265
- // {"payload":{"state":"online"},"topic":"FL.Licht.Links/availability"} ----> {"payload":{"available":true},"topic":"FL.Licht.Links"}
266
- const newMessage = {
267
- payload: { available: messageObj.payload.state == 'online' },
268
- topic: messageObj.topic.replace('/availability', ''),
269
- };
270
-
271
- statesController.processDeviceMessage(newMessage);
272
- }
273
- // States
274
- } else {
275
- // With the MQTT output type attribute_and_json, primitive payloads arrive here that must be discarded.
276
- if (utils.isObject(messageObj.payload) == false) {
277
- return;
278
- }
279
- // If MQTT is used, I have to filter the self-sent 'set' commands.
280
- if (messageObj.topic.endsWith('/set')) {
281
- return;
282
- }
283
-
284
- 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;
285
332
  }
333
+ if (messageObj.topic.endsWith('/set')) {
334
+ break;
335
+ }
336
+ await this.statesController.processDeviceMessage(messageObj);
286
337
  }
287
338
  break;
339
+ }
288
340
  }
289
341
  } finally {
342
+ // Mutex immer freigeben
290
343
  release();
291
344
  }
292
345
  }
293
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
+
294
456
  async onUnload(callback) {
295
- // Close MQTT connections
296
- if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
297
- 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') {
298
477
  try {
299
- if (mqttClient) {
300
- mqttClient.end();
478
+ if (this.websocketController) {
479
+ this.websocketController.closeConnection();
301
480
  }
302
481
  } catch (e) {
303
482
  this.log.error(e);
304
483
  }
305
484
  }
306
- }
307
- // Internal or Dummy MQTT-Server
308
- if (this.config.connectionType == 'intmqtt' || this.config.dummyMqtt == true) {
309
485
  try {
310
- if (mqttServerController) {
311
- mqttServerController.closeServer();
486
+ if (this.statesController) {
487
+ await this.statesController.setAllAvailableToFalse();
312
488
  }
313
489
  } catch (e) {
314
490
  this.log.error(e);
315
491
  }
316
- } else if (this.config.connectionType == 'ws') {
317
- // Websocket
318
492
  try {
319
- if (websocketController) {
320
- websocketController.closeConnection();
493
+ if (this.websocketController) {
494
+ await this.websocketController.allTimerClear();
321
495
  }
322
496
  } catch (e) {
323
497
  this.log.error(e);
324
498
  }
325
- }
326
- // Set all device available states of false
327
- try {
328
- if (statesController) {
329
- await statesController.setAllAvailableToFalse();
330
- }
331
- } catch (e) {
332
- this.log.error(e);
333
- }
334
- // Clear all websocket timers
335
- try {
336
- if (websocketController) {
337
- await websocketController.allTimerClear();
338
- }
339
- } catch (e) {
340
- this.log.error(e);
341
- }
342
- // Clear all state timers
343
- try {
344
- if (statesController) {
345
- await statesController.allTimerClear();
499
+ try {
500
+ if (this.statesController) {
501
+ await this.statesController.allTimerClear();
502
+ }
503
+ } catch (e) {
504
+ this.log.error(e);
346
505
  }
347
- } catch (e) {
348
- this.log.error(e);
349
- }
350
506
 
351
- this.setState('info.connection', false, true);
507
+ this.setState('info.connection', false, true);
352
508
 
353
- 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
+ }
354
519
  }
355
520
 
356
521
  async onStateChange(id, state) {
357
- if (state && state.ack == false) {
522
+ if (state && state.ack === false) {
358
523
  if (id.endsWith('info.debugmessages')) {
359
- logCustomizations.debugDevices = state.val;
524
+ this.logCustomizations.debugDevices = String(state.val || '');
360
525
  this.setState(id, state.val, true);
361
526
  return;
362
527
  }
363
528
  if (id.endsWith('info.logfilter')) {
364
- logCustomizations.logfilter = state.val.split(';').filter((x) => x); // filter removes empty strings here
529
+ this.logCustomizations.logfilter = String(state.val || '').split(';').filter((x) => x);
365
530
  this.setState(id, state.val, true);
366
531
  return;
367
532
  }
368
533
 
369
- 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
+ }
370
543
 
371
544
  if (['exmqtt', 'intmqtt'].includes(this.config.connectionType)) {
372
- mqttClient.publish(`${this.config.baseTopic}/${message.topic}`, JSON.stringify(message.payload));
373
- } else if (this.config.connectionType == 'ws') {
374
- 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 }));
375
568
  }
376
569
  }
377
570
  }
378
571
  }
379
572
 
380
573
  if (require.main !== module) {
381
- // Export the constructor in compact mode
382
574
  /**
383
575
  * @param {Partial<core.AdapterOptions>} [options]
384
576
  */
385
577
  module.exports = (options) => new Zigbee2mqtt(options);
386
578
  } else {
387
- // otherwise start the instance directly
388
579
  new Zigbee2mqtt();
389
580
  }