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/README.md CHANGED
@@ -22,6 +22,15 @@ This adapter allows to control the data points of the devices of a Zigbee2MQTT i
22
22
  [Adapter Documentation](https://github.com/arteck/ioBroker.zigbee2mqtt/blob/main/docs/wiki.md)
23
23
 
24
24
  ## Changelog
25
+ ### 3.1.1 (2026-04-21)
26
+ * (arteck) fix connection handling
27
+
28
+ ### 3.1.0 (2026-04-20)
29
+ * (arteck) refactoring
30
+
31
+ ### 3.0.22 (2026-04-12)
32
+ * (arteck) Dependencies have been updated
33
+
25
34
  ### 3.0.21 (2026-01-29)
26
35
  * (arteck) add coordinator status info
27
36
 
@@ -560,6 +560,43 @@
560
560
  "lg": 12,
561
561
  "xl": 12,
562
562
  "newLine": true
563
+ },
564
+ "spacerDeleteStates": {
565
+ "type": "staticText",
566
+ "text": "",
567
+ "xs": 12,
568
+ "newLine": true
569
+ },
570
+ "headerDeleteStates": {
571
+ "type": "header",
572
+ "text": "coming soon ",
573
+ "size": 4,
574
+ "newLine": true
575
+ },
576
+ "buttonDeleteOldStates": {
577
+ "type": "sendTo",
578
+ "command": "deleteOldStates",
579
+ "label": "Delete old / unused states",
580
+ "variant": "contained",
581
+ "color": "secondary",
582
+ "icon": "delete",
583
+ "disabled": "1 === 1",
584
+ "xs": 12,
585
+ "sm": 6,
586
+ "md": 4,
587
+ "lg": 3,
588
+ "xl": 3,
589
+ "newLine": true
590
+ },
591
+ "_deleteOldStatesResult": {
592
+ "type": "staticText",
593
+ "text": "",
594
+ "xs": 12,
595
+ "newLine": false,
596
+ "style": {
597
+ "marginTop": 8,
598
+ "fontStyle": "italic"
599
+ }
563
600
  }
564
601
  }
565
602
  }
package/io-package.json CHANGED
@@ -1,8 +1,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zigbee2mqtt",
4
- "version": "3.0.21",
4
+ "version": "3.1.1",
5
5
  "news": {
6
+ "3.1.1": {
7
+ "en": "fix connection handling",
8
+ "de": "befestigungstechnik",
9
+ "ru": "фиксировать связь",
10
+ "pt": "corrigir o tratamento da ligação",
11
+ "nl": "fix verbinding handling",
12
+ "fr": "fixer la manipulation de la connexion",
13
+ "it": "gestione del collegamento",
14
+ "es": "corrección de conexión",
15
+ "pl": "naprawić obsługę połączenia",
16
+ "uk": "кріплення з'єднання",
17
+ "zh-cn": "固定连接处理"
18
+ },
6
19
  "3.0.21": {
7
20
  "en": "add coordinator status info",
8
21
  "de": "koordinator statusinfo hinzufügen",
@@ -54,45 +67,6 @@
54
67
  "pl": "typo",
55
68
  "uk": "типи",
56
69
  "zh-cn": "类型"
57
- },
58
- "3.0.17": {
59
- "en": "typo",
60
- "de": "typo",
61
- "ru": "опечатка",
62
- "pt": "erro de digitação",
63
- "nl": "type",
64
- "fr": "typo",
65
- "it": "tipo",
66
- "es": "typo",
67
- "pl": "typo",
68
- "uk": "типи",
69
- "zh-cn": "类型"
70
- },
71
- "3.0.16": {
72
- "en": "fix action dp",
73
- "de": "funktion dp",
74
- "ru": "исправить действие dp",
75
- "pt": "corrigir ação dp",
76
- "nl": "fix actie dp",
77
- "fr": "corriger l'action dp",
78
- "it": "fix azione dp",
79
- "es": "arreglar acción dp",
80
- "pl": "fix action dp",
81
- "uk": "фіксувати дію dp",
82
- "zh-cn": "固定动作 dp"
83
- },
84
- "3.0.15": {
85
- "en": "update",
86
- "de": "aktualisierung",
87
- "ru": "обновление",
88
- "pt": "atualizar",
89
- "nl": "bijwerken",
90
- "fr": "mise à jour",
91
- "it": "aggiornamento",
92
- "es": "actualización",
93
- "pl": "aktualizacja",
94
- "uk": "оновлення",
95
- "zh-cn": "更新"
96
70
  }
97
71
  },
98
72
  "messages": [
@@ -220,12 +194,12 @@
220
194
  },
221
195
  "dependencies": [
222
196
  {
223
- "js-controller": ">= 5.0.19"
197
+ "js-controller": ">= 6.0.11"
224
198
  }
225
199
  ],
226
200
  "globalDependencies": [
227
201
  {
228
- "admin": ">=7.4.10"
202
+ "admin": ">=7.6.20"
229
203
  }
230
204
  ]
231
205
  },
package/lib/check.js CHANGED
@@ -5,28 +5,36 @@
5
5
  * @param version
6
6
  */
7
7
  function checkConfig(config, log, version) {
8
+ if (!config || !log) {
9
+ return;
10
+ }
11
+
12
+ const advanced = config.advanced || {};
13
+ const deviceOptions = config.device_options || {};
14
+
8
15
  const checkAPIOptions = {
9
- legacy_api_enabled: config.advanced.legacy_api != false,
10
- legacy_availability_payload_enabled: config.advanced.legacy_availability_payload != false,
11
- device_legacy_enabled: config.device_options.legacy != false
16
+ legacy_api_enabled: advanced.legacy_api !== false,
17
+ legacy_availability_payload_enabled: advanced.legacy_availability_payload !== false,
18
+ device_legacy_enabled: deviceOptions.legacy !== false,
12
19
  };
13
20
 
14
21
  const checkAPIOptionsOutput = {
15
- payload_contains_not_json: config.advanced.output != 'attribute_and_json' && config.advanced.output != 'json'
22
+ payload_contains_not_json: advanced.output !== 'attribute_and_json' && advanced.output !== 'json',
16
23
  };
17
24
 
18
- if (version.startsWith('1.')) { // wird in version 2.x immer auf false gesetzt sein
19
- if (Object.values(checkAPIOptions).filter((x) => x == true).length > 0) {
25
+ // Fix: version kann undefined/null sein (ältere Z2M-Versionen)
26
+ if (version && version.startsWith('1.')) {
27
+ if (Object.values(checkAPIOptions).filter((x) => x === true).length > 0) {
20
28
  log.error('===================================================');
21
29
  log.error('===================================================');
22
- if (checkAPIOptions.legacy_api_enabled == true) {
30
+ if (checkAPIOptions.legacy_api_enabled === true) {
23
31
  log.error('Legacy api is activated, so the adapter can not work correctly!!!');
24
32
  log.error('Please add the following lines to your Zigbee2MQTT configuration.yaml:');
25
33
  log.error('advanced:');
26
34
  log.error(' legacy_api: false');
27
35
  log.error('');
28
36
  }
29
- if (checkAPIOptions.legacy_availability_payload_enabled == true) {
37
+ if (checkAPIOptions.legacy_availability_payload_enabled === true) {
30
38
  log.error(
31
39
  'Legacy Availability Payload is activated, thus the adapter cannot represent the availability of the devices!!!'
32
40
  );
@@ -35,7 +43,7 @@ function checkConfig(config, log, version) {
35
43
  log.error('legacy_availability_payload: false');
36
44
  log.error('');
37
45
  }
38
- if (checkAPIOptions.device_legacy_enabled == true) {
46
+ if (checkAPIOptions.device_legacy_enabled === true) {
39
47
  log.error(
40
48
  'Device Legacy Payload is activated, therefore the adapter may process the states of the devices correctly!!!'
41
49
  );
@@ -46,8 +54,8 @@ function checkConfig(config, log, version) {
46
54
  log.error('===================================================');
47
55
  }
48
56
  }
49
- if (Object.values(checkAPIOptionsOutput).filter((x) => x == true).length > 0) {
50
- if (checkAPIOptions.payload_contains_not_json == true ) {
57
+ if (Object.values(checkAPIOptionsOutput).filter((x) => x === true).length > 0) {
58
+ if (checkAPIOptions.payload_contains_not_json === true ) {
51
59
  log.error('===================================================');
52
60
  log.error(
53
61
  'MQTT output type must "attribute_and_json" or "json" , therefore the adapter may process the states of the devices correctly!!!'
@@ -33,8 +33,17 @@ class DeviceController {
33
33
  * @param devicesMessage
34
34
  */
35
35
  async createDeviceDefinitions(devicesMessage) {
36
+ // Fix 1: devicesMessage kann null/kein Array sein (z.B. leerer bridge/devices payload)
37
+ if (!Array.isArray(devicesMessage)) {
38
+ this.adapter.log.warn('createDeviceDefinitions: payload ist kein Array');
39
+ return;
40
+ }
36
41
  utils.clearArray(this.deviceCache);
37
42
  for (const devicesMessag of devicesMessage) {
43
+ if (!devicesMessag || !devicesMessag.ieee_address) {
44
+ continue;
45
+ }
46
+
38
47
  if (this.logCustomizations.debugDevices.includes(devicesMessag.ieee_address)) {
39
48
  this.adapter.log.warn(
40
49
  `--->>> fromZ2M -> ${devicesMessag.ieee_address} exposes: ${JSON.stringify(devicesMessag)}`
@@ -42,16 +51,17 @@ class DeviceController {
42
51
  }
43
52
 
44
53
  if (devicesMessag.definition != null) {
45
- // if the device is already present in the cache, remove it
46
54
  this.removeDeviceByIeee(this.deviceCache, devicesMessag.ieee_address);
47
55
 
48
- if (devicesMessag.definition.exposes) {
56
+ // Fix 3: exposes muss auch ein Array sein
57
+ if (Array.isArray(devicesMessag.definition.exposes)) {
49
58
  try {
50
59
  const newDevice = await createDeviceFromExposes(devicesMessag, this.adapter);
51
60
  newDevice.icon = await this.imageController.getDeviceIcon(devicesMessag);
52
61
  this.deviceCache.push(newDevice);
53
62
  } catch (err) {
54
- this.adapter.log.warn(`Cannot ${devicesMessag.friendly_name} create Device from Exposes!`);
63
+ // Fix 2: friendly_name im Fehlerlog verwenden
64
+ this.adapter.log.warn(`Cannot create Device from Exposes for ${devicesMessag.friendly_name || devicesMessag.ieee_address}!`);
55
65
  this.adapter.log.debug(JSON.stringify(devicesMessag));
56
66
  this.adapter.log.debug(err);
57
67
  }
@@ -67,23 +77,22 @@ class DeviceController {
67
77
  * @param scenes
68
78
  */
69
79
  async defineGroupDevice(groupID, ieee_address, scenes) {
70
- const brmPropName =
71
- this.adapter.config.brightnessMoveOnOff == true ? 'brightness_move_onoff' : 'brightness_move';
72
- const brsPropName =
73
- this.adapter.config.brightnessStepOnOff == true ? 'brightness_step_onoff' : 'brightness_step';
80
+ const brmPropName = this.adapter.config.brightnessMoveOnOff === true ? 'brightness_move_onoff' : 'brightness_move';
81
+ const brsPropName = this.adapter.config.brightnessStepOnOff === true ? 'brightness_step_onoff' : 'brightness_step';
74
82
  const newDevice = {
75
83
  id: groupID,
76
84
  ieee_address: ieee_address,
77
85
  icon: undefined, // await imageController.getDeviceIcon(devicesMessag), device.definition.model
78
86
  optionsValues: {},
79
87
  states: [
80
- statesDefs.available,
81
- statesDefs.brightness,
82
- statesDefs.colortemp_move,
83
- statesDefs.transition,
88
+ // Fix 1: Klone aller statesDefs-Objekte – sonst werden geteilte Referenzen mutiert
89
+ Object.assign({}, statesDefs.available),
90
+ Object.assign({}, statesDefs.brightness),
91
+ Object.assign({}, statesDefs.colortemp_move),
92
+ Object.assign({}, statesDefs.transition),
84
93
  {
85
94
  id: 'state',
86
- prob: 'state',
95
+ prop: 'state',
87
96
  name: 'Switch state',
88
97
  options: ['transition'],
89
98
  icon: undefined,
@@ -133,29 +142,28 @@ class DeviceController {
133
142
  type: 'string',
134
143
  def: '#ff00ff',
135
144
  setter: (value) => {
136
- let xy = [0, 0];
145
+ // Fix 6: redundante [0,0]-Initialisierung entfernt
137
146
  const rgbcolor = colors.ParseColor(value);
138
- xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
147
+ const xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
139
148
  return {
140
149
  x: xy[0],
141
150
  y: xy[1],
142
151
  };
143
152
  },
144
153
  getter: (payload) => {
145
- if (payload.color_mode != 'xy' && this.config.colorTempSyncColor == false) {
154
+ if (payload.color_mode !== 'xy' && !this.config.colorTempSyncColor) {
146
155
  return undefined;
147
156
  }
148
- if (payload.color && payload.color.x && payload.color.y) {
157
+ // Fix 2: x=0 / y=0 sind gültige CIE-Koordinaten → != null statt truthy
158
+ if (payload.color && payload.color.x != null && payload.color.y != null) {
149
159
  const colorval = rgb.cie_to_rgb(payload.color.x, payload.color.y);
150
160
  return (
151
- `#${
152
- utils.decimalToHex(colorval[0])
153
- }${utils.decimalToHex(colorval[1])
154
- }${utils.decimalToHex(colorval[2])}`
161
+ `#${utils.decimalToHex(colorval[0])
162
+ }${utils.decimalToHex(colorval[1])
163
+ }${utils.decimalToHex(colorval[2])}`
155
164
  );
156
- }
157
- return undefined;
158
-
165
+ }
166
+ return undefined;
159
167
  },
160
168
  },
161
169
  {
@@ -168,22 +176,21 @@ class DeviceController {
168
176
  write: true,
169
177
  read: true,
170
178
  type: 'number',
171
- min: this.config.useKelvin == true ? utils.miredKelvinConversion(550) : 150,
172
- max: this.config.useKelvin == true ? utils.miredKelvinConversion(153) : 500,
173
- def: this.config.useKelvin == true ? utils.miredKelvinConversion(153) : 500,
174
- unit: this.config.useKelvin == true ? 'K' : 'mired',
179
+ min: this.config.useKelvin === true ? utils.miredKelvinConversion(550) : 150,
180
+ max: this.config.useKelvin === true ? utils.miredKelvinConversion(153) : 500,
181
+ def: this.config.useKelvin === true ? utils.miredKelvinConversion(153) : 500,
182
+ unit: this.config.useKelvin === true ? 'K' : 'mired',
175
183
  setter: (value) => {
176
184
  return utils.toMired(value);
177
185
  },
178
186
  getter: (payload) => {
179
- if (payload.color_mode != 'color_temp') {
187
+ if (payload.color_mode !== 'color_temp') {
180
188
  return undefined;
181
189
  }
182
- if (this.config.useKelvin == true) {
190
+ if (this.config.useKelvin === true) {
183
191
  return utils.miredKelvinConversion(payload.color_temp);
184
- }
185
- return payload.color_temp;
186
-
192
+ }
193
+ return payload.color_temp;
187
194
  },
188
195
  },
189
196
  {
@@ -248,12 +255,21 @@ class DeviceController {
248
255
  * @param groupsMessage
249
256
  */
250
257
  async createGroupDefinitions(groupsMessage) {
258
+ // Fix 4: groupsMessage kann null/kein Array sein
259
+ if (!Array.isArray(groupsMessage)) {
260
+ this.adapter.log.warn('createGroupDefinitions: payload ist kein Array');
261
+ return;
262
+ }
251
263
  utils.clearArray(this.groupCache);
252
264
  for (const groupMessage of groupsMessage) {
253
- if (this.logCustomizations.debugDevices.includes(groupMessage.id)) {
265
+ // Fix 5: fehlende Pflichtfelder abfangen
266
+ if (!groupMessage || groupMessage.id == null || !groupMessage.friendly_name) {
267
+ continue;
268
+ }
269
+ if (this.logCustomizations.debugDevices.includes(String(groupMessage.id))) {
254
270
  this.adapter.log.warn(`--->>> fromZ2M -> ${groupMessage.id} exposes: ${JSON.stringify(groupMessage)}`);
255
271
  }
256
- await this.defineGroupDevice(groupMessage.friendly_name, `group_${groupMessage.id}`, groupMessage.scenes);
272
+ await this.defineGroupDevice(groupMessage.friendly_name, `group_${groupMessage.id}`, groupMessage.scenes || []);
257
273
  }
258
274
  }
259
275
 
@@ -261,144 +277,158 @@ class DeviceController {
261
277
  *
262
278
  */
263
279
  async createOrUpdateDevices() {
264
- for (const device of this.groupCache.concat(this.deviceCache)) {
265
- let deviceName = await this.getDeviceName(device);
266
- let description = await this.getDeviceDescription(device);
280
+ // Gruppen und Geräte getrennt verarbeiten
281
+ for (const device of this.groupCache) {
282
+ await this._createOrUpdateSingleDevice(device, true);
283
+ }
284
+ for (const device of this.deviceCache) {
285
+ await this._createOrUpdateSingleDevice(device, false);
286
+ }
287
+ }
267
288
 
268
- if (deviceName == '' && device.description) {
269
- deviceName = device.description;
270
- description = '';
271
- }
289
+ /**
290
+ * @param device
291
+ * @param isGroup
292
+ */
293
+ async _createOrUpdateSingleDevice(device, isGroup) {
294
+ let deviceName = await this.getDeviceName(device);
295
+ let description = await this.getDeviceDescription(device);
272
296
 
273
- // Manipulate deviceName if the device is disabled, so the update of the device is triggered as well
274
- if (device.disabled && device.disabled == true) {
275
- if (this.config.useEventInDesc == true) {
276
- description = 'Device is disabled!';
277
- } else {
278
- deviceName = `[Disabled] ${deviceName}`;
279
- }
297
+ if (deviceName === '' && device.description) {
298
+ deviceName = device.description;
299
+ description = '';
300
+ }
301
+
302
+ // Disabled-Flag nur bei echten Geräten – Gruppen können nicht disabled sein
303
+ if (!isGroup && device.disabled === true) {
304
+ if (this.config.useEventInDesc === true) {
305
+ description = 'Device is disabled!';
306
+ } else {
307
+ deviceName = `[Disabled] ${deviceName}`;
280
308
  }
309
+ }
281
310
 
282
- if (!this.createCache[device.ieee_address] || this.createCache[device.ieee_address].name != deviceName || this.createCache[device.ieee_address].description != description) {
283
- const deviceObj = {
284
- type: 'device',
285
- common: {
286
- icon: device.icon,
287
- name: deviceName,
288
- desc: description,
289
- statusStates: { onlineId: '' },
290
- },
291
- native: {
292
- deviceRemoved: false,
293
- groupDevice: false,
294
- },
295
- };
311
+ if (!this.createCache[device.ieee_address] ||
312
+ this.createCache[device.ieee_address].name !== deviceName ||
313
+ this.createCache[device.ieee_address].description !== description) {
296
314
 
297
- // Group Device
298
- if (device.ieee_address.includes('group_')) {
299
- deviceObj.native.groupDevice = true;
300
- deviceObj.common.statusStates.onlineId = `${this.adapter.name}.${this.adapter.instance}.${device.ieee_address}.available`;
301
- }
302
- // Disabled Device
303
- else if (device.disabled || device.disabled == true) {
304
- // Placeholder for possible later logic
305
- }
306
- // Only the onlineId is set if the device is not disabled
307
- else {
308
- deviceObj.common.statusStates.onlineId = `${this.adapter.name}.${this.adapter.instance}.${device.ieee_address}.available`;
309
- }
310
- await this.adapter.extendObjectAsync(device.ieee_address, deviceObj);
311
- this.createCache[device.ieee_address] = { name: deviceName, description: description };
312
- }
315
+ const deviceObj = {
316
+ type: 'device',
317
+ common: {
318
+ icon: device.icon,
319
+ name: deviceName,
320
+ desc: description,
321
+ statusStates: { onlineId: '' },
322
+ },
323
+ native: {
324
+ deviceRemoved: false,
325
+ groupDevice: isGroup,
326
+ },
327
+ };
313
328
 
314
- // Here it is checked whether the scenes match the current data from z2m.
315
- // If necessary, scenes are automatically deleted from ioBroker.
316
- const sceneStates = await this.adapter.getStatesAsync(`${device.ieee_address}.scene_*`);
317
- const sceneIDs = Object.keys(sceneStates);
318
- for (const sceneID of sceneIDs) {
319
- const stateID = sceneID.split('.')[3];
320
- if (device.states.find((x) => x.id == stateID) == null) {
321
- this.adapter.delObject(sceneID);
322
- }
329
+ if (isGroup || device.disabled !== true) {
330
+ deviceObj.common.statusStates.onlineId = `${this.adapter.name}.${this.adapter.instance}.${device.ieee_address}.available`;
323
331
  }
324
332
 
325
- if (device != undefined && device.states != undefined) {
326
- for (const state of device.states) {
327
- if (state && (!this.createCache[device.ieee_address][state.id] || this.createCache[device.ieee_address][state.id].name != state.name)) {
328
- const iobState = {
329
- type: 'state',
330
- common: await this.copyAndCleanStateObj(state),
331
- native: {},
332
- };
333
+ await this.adapter.extendObjectAsync(device.ieee_address, deviceObj);
334
+ this.createCache[device.ieee_address] = { name: deviceName, description: description };
335
+ }
333
336
 
334
- await this.adapter.extendObjectAsync(`${device.ieee_address}.${state.id}`, iobState);
335
- this.createCache[device.ieee_address][state.id] = {name: state.name, created: true};
336
- }
337
+ if (!Array.isArray(device.states)) {
338
+ return;
339
+ }
340
+
341
+ // Veraltete scene_*-States bereinigen – getForeignObjectsAsync mit vollem Namespace-Pfad
342
+ const sceneObjects = await this.adapter.getForeignObjectsAsync(
343
+ `${this.adapter.namespace}.${device.ieee_address}.scene_*`
344
+ );
345
+ if (sceneObjects) {
346
+ for (const sceneObjId of Object.keys(sceneObjects)) {
347
+ const parts = sceneObjId.split('.');
348
+ const stateID = parts[parts.length - 1];
349
+ if (!device.states.find((x) => x.id === stateID)) {
350
+ await this.adapter.delForeignObjectAsync(sceneObjId);
337
351
  }
338
352
  }
339
353
  }
340
- }
341
354
 
342
- /**
343
- *
344
- * @param messageObj
345
- */
346
- async renameDeviceInCache(messageObj) {
347
- const renamedDevice = this.groupCache
348
- .concat(this.deviceCache)
349
- .find((x) => x.id == messageObj.payload.data.from);
350
- if (renamedDevice) {
351
- renamedDevice.id = messageObj.payload.data.to;
355
+ for (const state of device.states) {
356
+ if (state && (!this.createCache[device.ieee_address][state.id] ||
357
+ this.createCache[device.ieee_address][state.id].name !== state.name)) {
358
+
359
+ const iobState = {
360
+ type: 'state',
361
+ common: await this.copyAndCleanStateObj(state),
362
+ native: {},
363
+ };
364
+
365
+ await this.adapter.extendObjectAsync(`${device.ieee_address}.${state.id}`, iobState);
366
+ this.createCache[device.ieee_address][state.id] = { name: state.name, created: true };
367
+ }
352
368
  }
353
369
  }
354
370
 
355
371
  /**
356
- *
372
+ * Nur echte Geräte prüfen – Gruppen werden hier nie verarbeitet
357
373
  */
358
374
  async checkAndProgressDeviceRemove() {
359
- let description = '';
360
- let deviceName = '';
361
375
  let iobDevices = await this.adapter.getDevicesAsync();
362
- // Do not consider devices already marked as "deviceRemoved"
363
- iobDevices = iobDevices.filter((x) => x.native.deviceRemoved == false);
364
- // Do not consider groups
365
- iobDevices = iobDevices.filter((x) => x.native.groupDevice == false);
376
+ // Nur nicht-entfernte, echte Geräte (keine Gruppen)
377
+ iobDevices = iobDevices.filter((x) => x.native && x.native.deviceRemoved === false);
378
+ iobDevices = iobDevices.filter((x) => x.native && x.native.groupDevice === false);
366
379
 
367
380
  for (const iobDevice of iobDevices) {
368
- const ieee_address = iobDevice._id.split('.')[2];
369
- //Check whether the devices found from the object tree are also available in the DeviceCache
370
- if (!this.deviceCache.find((x) => x.ieee_address == ieee_address)) {
371
- deviceName = iobDevice.common.name;
381
+ const idParts = iobDevice._id.split('.');
382
+ if (idParts.length < 3) {continue;}
383
+ const ieee_address = idParts[2];
372
384
 
373
- if (this.config.useEventInDesc == true) {
385
+ // Gruppen-Einträge im ioBroker-Baum explizit überspringen
386
+ if (ieee_address.startsWith('group_')) {continue;}
387
+
388
+ if (!this.deviceCache.find((x) => x.ieee_address === ieee_address)) {
389
+ let deviceName = iobDevice.common && iobDevice.common.name ? iobDevice.common.name : ieee_address;
390
+ let description = '';
391
+
392
+ if (this.config.useEventInDesc === true) {
374
393
  description = 'Device was removed!';
375
394
  } else {
376
395
  deviceName = `[Removed] ${deviceName}`;
377
396
  }
378
397
 
379
- this.adapter.extendObject(`${ieee_address}`, {
380
- common: {
381
- name: deviceName,
382
- desc: description,
383
- },
384
- native: {
385
- deviceRemoved: true,
386
- },
398
+ await this.adapter.extendObjectAsync(ieee_address, {
399
+ common: { name: deviceName, desc: description },
400
+ native: { deviceRemoved: true },
387
401
  });
388
- this.adapter.setStateChangedAsync(`${ieee_address}.available`, false, true);
402
+ await this.adapter.setStateChangedAsync(`${ieee_address}.available`, false, true);
389
403
 
390
404
  delete this.createCache[ieee_address];
391
405
  }
392
406
  }
393
407
  }
394
408
 
409
+ /**
410
+ *
411
+ * @param messageObj
412
+ */
413
+ async renameDeviceInCache(messageObj) {
414
+ if (!messageObj.payload || !messageObj.payload.data) {
415
+ return;
416
+ }
417
+ const renamedDevice = this.groupCache
418
+ .concat(this.deviceCache)
419
+ .find((x) => x.id === messageObj.payload.data.from);
420
+ if (renamedDevice) {
421
+ renamedDevice.id = messageObj.payload.data.to;
422
+ }
423
+ }
424
+
395
425
  /**
396
426
  *
397
427
  * @param devices
398
428
  * @param ieee_address
399
429
  */
400
430
  removeDeviceByIeee(devices, ieee_address) {
401
- const idx = devices.findIndex((x) => x.ieee_address == ieee_address);
431
+ const idx = devices.findIndex((x) => x.ieee_address === ieee_address);
402
432
  if (idx > -1) {
403
433
  devices.splice(idx, 1);
404
434
  }
@@ -434,7 +464,7 @@ class DeviceController {
434
464
  * @param device
435
465
  */
436
466
  getDeviceName(device) {
437
- return device.id == device.ieee_address ? '' : device.id;
467
+ return device.id === device.ieee_address ? '' : device.id;
438
468
  }
439
469
 
440
470
  /**
@@ -456,7 +486,11 @@ class DeviceController {
456
486
  this.adapter.setState('info.missing_routers_count', missingRoutersCount, true);
457
487
 
458
488
  if (missingRoutersCount > 0) {
459
- this.adapter.log[this.config.coordinatorCheckLogLvl](
489
+ const logLvl = this.config.coordinatorCheckLogLvl;
490
+ const logFn = (logLvl && typeof this.adapter.log[logLvl] === 'function')
491
+ ? this.adapter.log[logLvl].bind(this.adapter.log)
492
+ : this.adapter.log.warn.bind(this.adapter.log);
493
+ logFn(
460
494
  `Coordinator check: ${missingRoutersCount} missing routers were found, please check the data point 'zigbee2mqtt.x.info.missing_routers'!`
461
495
  );
462
496
  } else {