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.
package/README.md CHANGED
@@ -22,91 +22,23 @@ 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.5 (2026-04-23)
25
+ ### 3.2.1 (2026-05-05)
26
+ * (copilot) Adapter requires node.js >= 22 now
27
+ * (arteck) upd device manager
28
+ * (arteck) fix aedes-persistence
26
29
  * (arteck) fix illuminance
27
30
 
28
- ### 3.1.4 (2026-04-23)
29
- * (arteck) fix illuminance if not exists. we calculate it ourselves
31
+ ### 3.2.0 (2026-04-26)
32
+ * (arteck) del deprectated setStateAsync
30
33
 
31
- ### 3.1.3 (2026-04-23)
32
- * (arteck) fix illuminance_raw
33
- * (arteck) fix illuminance
34
- * (arteck) fix action state
35
-
36
- ### 3.1.2 (2026-04-23)
37
- * (arteck) add test
38
-
39
- ### 3.1.1 (2026-04-21)
40
- * (arteck) fix connection handling
41
-
42
- ### 3.1.0 (2026-04-20)
43
- * (arteck) refactoring
44
-
45
- ### 3.0.22 (2026-04-12)
46
- * (arteck) Dependencies have been updated
47
-
48
- ### 3.0.21 (2026-01-29)
49
- * (arteck) add coordinator status info
50
-
51
- ### 3.0.20 (2026-01-29)
52
- * (arteck) check payload if undefined
53
-
54
- ### 3.0.19 (2026-01-28)
55
- * (arteck) modify action dp
56
-
57
- ### 3.0.18 (2026-01-28)
58
- * (arteck) typo
59
-
60
- ### 3.0.17 (2026-01-28)
61
- * (arteck) typo
62
-
63
- ### 3.0.16 (2026-01-28)
64
- * (arteck) fix action dp
65
-
66
- ### 3.0.15 (2026-01-27)
67
- * (arteck) update
68
-
69
- ### 3.0.14 (2026-01-27)
70
- * (arteck) back to sharp 0.33.5
71
-
72
- ### 3.0.13 (2026-01-25)
73
- * (arteck) add action dp
74
-
75
- ### 3.0.12 (2026-01-05)
76
- * (arteck) Dependencies have been updated
77
- * (MMeinhardt1) typo fix
78
-
79
- ### 3.0.11 (2025-12-31)
80
- - (arteck) fix info.connection
81
-
82
- ### 3.0.10 (2025-12-07)
83
- - (arteck) Dependencies have been updated
84
- - (bluefox) Changed role of `color_temp_startup` state to `level` to avoid double `level.temperature` in one device
85
- - (arteck) fix ZBMINIR2 inching DP
86
- - (arteck) delete DP colortempstartup
87
-
88
- ### 3.0.9 (2025-06-19)
89
- - (bjoernbusch) queue up message parsing
90
- - (dotcom84) Support for non-default MQTT base topics
91
-
92
- ### 3.0.8 (2025-06-08)
93
- - (arteck) fix device is deleted
94
- - (arteck) fix translation
95
-
96
- ### 3.0.7 (2025-06-07)
97
- - (arteck) fix jsonconf
98
-
99
- ### 3.0.6 (2025-05-31)
100
- - (arteck) settings restructure
101
- - (arteck) fix icon not found message
102
-
103
- .
104
- .
105
- .
34
+ ### 3.1.9 (2026-04-26)
35
+ * (arteck) fix WS close
106
36
 
107
- ### 0.1.0 (2022-09-29)
37
+ ### 3.1.8 (2026-04-26)
38
+ * (arteck) back to old dependency aedes
108
39
 
109
- - (o0shojo0o) first release
40
+ ### 3.1.7 (2026-04-24)
41
+ * (arteck) fix illuminance = 0
110
42
 
111
43
  ## License
112
44
 
@@ -465,6 +465,11 @@
465
465
  "type": "header",
466
466
  "text": "Expert Settings. Please only use if you know what you're doing",
467
467
  "size": 2,
468
+ "xs": 12,
469
+ "sm": 12,
470
+ "md": 12,
471
+ "lg": 12,
472
+ "xl": 12,
468
473
  "newLine": true
469
474
  },
470
475
  "coordinatorCheck": {
@@ -591,7 +596,11 @@
591
596
  "_deleteOldStatesResult": {
592
597
  "type": "staticText",
593
598
  "text": "",
594
- "xs": 12,
599
+ "xs": 12,
600
+ "sm": 6,
601
+ "md": 4,
602
+ "lg": 3,
603
+ "xl": 3,
595
604
  "newLine": false,
596
605
  "style": {
597
606
  "marginTop": 8,
package/io-package.json CHANGED
@@ -1,8 +1,47 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zigbee2mqtt",
4
- "version": "3.1.5",
4
+ "version": "3.2.1",
5
5
  "news": {
6
+ "3.2.1": {
7
+ "en": "Adapter requires node.js >= 22 now\nupd device manager\nfix aedes-persistence\nfix illuminance",
8
+ "de": "Adapter benötigt node.js >= 22 jetzt\nupd device manager\naedes-persistence reparieren\nbefestigungsleuchten",
9
+ "ru": "Адаптер требует node.js >= 22 сейчас\nобновленный device manager\nфиксировать стойкость\nфиксировать освещенность",
10
+ "pt": "Adaptador requer nod.js >= 22 agora\ngerenciador de dispositivos upd\ncorrigir aedes-persistência\nfixar a iluminação",
11
+ "nl": "Voor de adapter zijn node.js < 22 nu nodig\nupd apparaatbeheer\nfix aedes-persistence\nfix verlichtingssterkte",
12
+ "fr": "Adaptateur nécessite node.js >= 22 maintenant\ngestionnaire de périphérique mis à jour\nfixer la persistance des aedes\nfixer l'éclairage",
13
+ "it": "Adattatore richiede node.js >= 22 ora\nupd device manager\nfissare aedes-persistenza\nilluminazione di correzione",
14
+ "es": "Adaptador requiere node.js ю= 22 ahora\nupd device manager\narreglar aedes-persistence\niluminación de fijación",
15
+ "pl": "Adapter wymaga node.js > = 22\nmenedżer urządzenia upd\nfix aedes- trwałość\npodświetlenie",
16
+ "uk": "Адаптер вимагає node.js >= 22 тепер\nдиспетчер пристроїв\nфіксувати адисистентність\nфіксувати ілюмінанс",
17
+ "zh-cn": "适配器需要节点.js 现在22\n上调设备管理器\n固定死因-持久性\n修补灯光"
18
+ },
19
+ "3.2.0": {
20
+ "en": "del deprectated setStateAsync",
21
+ "de": "del deprected setStateAsync",
22
+ "ru": "обсуждение StateAsync",
23
+ "pt": "del deprectated setStateAsync",
24
+ "nl": "del deprectated setStateAsync",
25
+ "fr": "set dépréciéStateAsync",
26
+ "it": "del set deprettatoStateAsync",
27
+ "es": "del deprectated setStateAsync",
28
+ "pl": "del deprectated setStateAsync",
29
+ "uk": "deprectated setStateAsync",
30
+ "zh-cn": "已贬值的设置"
31
+ },
32
+ "3.1.9": {
33
+ "en": "fix WS close",
34
+ "de": "wS schließen",
35
+ "ru": "закрыть WS Close",
36
+ "pt": "corrigir WS fechar",
37
+ "nl": "wS sluiten herstellen",
38
+ "fr": "correction WS fermer",
39
+ "it": "fix WS chiudi",
40
+ "es": "arreglar WS close",
41
+ "pl": "fix WS close",
42
+ "uk": "застібка WS",
43
+ "zh-cn": "修复 WS 关闭"
44
+ },
6
45
  "3.1.5": {
7
46
  "en": "fix illuminance",
8
47
  "de": "befestigungsleuchten",
@@ -15,84 +54,6 @@
15
54
  "pl": "podświetlenie",
16
55
  "uk": "фіксувати ілюмінанс",
17
56
  "zh-cn": "修补灯光"
18
- },
19
- "3.1.4": {
20
- "en": "fix illuminance if not exists. we calculate it ourselves",
21
- "de": "fixe beleuchtungsstärke, wenn nicht vorhanden. wir berechnen es selbst",
22
- "ru": "установить свет, если его нет. мы сами вычисляем",
23
- "pt": "fixar a iluminação se não existir. nós mesmos calculamos",
24
- "nl": "fix verlichtingssterkte indien niet bestaat. we berekenen het zelf",
25
- "fr": "fixer l'éclairage s'il n'existe pas. nous le calculons nous-mêmes",
26
- "it": "fissare l'illuminazione se non esiste. noi lo calcoliamo noi stessi",
27
- "es": "fijar la iluminación si no existe. lo calculamos nosotros mismos",
28
- "pl": "naprawić oświetlenie, jeśli nie istnieje. sami to obliczymy",
29
- "uk": "зафіксувати ілюмінанс, якщо не існує. ми розраховуємо самі",
30
- "zh-cn": "如果不存在的话, 则固定灯光。 我们自己计算"
31
- },
32
- "3.1.3": {
33
- "en": "fix illuminance_raw \nfix illuminance\nfix action state",
34
- "de": "fix illuminance raw\nbefestigungsleuchten\nfixer aktionszustand",
35
- "ru": "осветительный прибор raw\nфиксировать освещенность\nопределить состояние",
36
- "pt": "corrigir illuminance raw\nfixar a iluminação\ncorrigir o estado da ação",
37
- "nl": "fix illuminantie raw\nfix verlichtingssterkte\nfix actiestatus",
38
- "fr": "correction de l'éclairage raw\nfixer l'éclairage\nfixer l'état d'action",
39
- "it": "correzione illuminazione raw\nilluminazione di correzione\nfix azione stato",
40
- "es": "iluminación raw\niluminación de fijación\nestado de acción",
41
- "pl": "fix lightinance _ raw\npodświetlenie\nstan działania",
42
- "uk": "фіксація талії raw\nфіксувати ілюмінанс\nфіксувати стан дії",
43
- "zh-cn": "修补灯光 raw\n修补灯光\n固定动作状态"
44
- },
45
- "3.1.2": {
46
- "en": "add test",
47
- "de": "test hinzufügen",
48
- "ru": "добавить тест",
49
- "pt": "adicionar teste",
50
- "nl": "test toevoegen",
51
- "fr": "ajouter un essai",
52
- "it": "aggiungere test",
53
- "es": "agregar la prueba",
54
- "pl": "dodać test",
55
- "uk": "додати тест",
56
- "zh-cn": "添加测试"
57
- },
58
- "3.1.1": {
59
- "en": "fix connection handling",
60
- "de": "befestigungstechnik",
61
- "ru": "фиксировать связь",
62
- "pt": "corrigir o tratamento da ligação",
63
- "nl": "fix verbinding handling",
64
- "fr": "fixer la manipulation de la connexion",
65
- "it": "gestione del collegamento",
66
- "es": "corrección de conexión",
67
- "pl": "naprawić obsługę połączenia",
68
- "uk": "кріплення з'єднання",
69
- "zh-cn": "固定连接处理"
70
- },
71
- "3.0.21": {
72
- "en": "add coordinator status info",
73
- "de": "koordinator statusinfo hinzufügen",
74
- "ru": "добавить информацию о статусе координатора",
75
- "pt": "adicionar informações sobre o estado do coordenador",
76
- "nl": "coordinator status info toevoegen",
77
- "fr": "ajouter le statut du coordonnateur info",
78
- "it": "aggiungere coordinate status info",
79
- "es": "añadir información sobre el estado de coordinador",
80
- "pl": "dodaj informacje o statusie koordynatora",
81
- "uk": "додати інформацію про статус координатора",
82
- "zh-cn": "添加协调员状态信息"
83
- },
84
- "3.0.20": {
85
- "en": "check payload if undefined",
86
- "de": "prüfen sie die nutzlast",
87
- "ru": "проверьте полезную нагрузку, если не определено",
88
- "pt": "verificar a carga útil se não for definida",
89
- "nl": "payload controleren indien niet gedefinieerd",
90
- "fr": "vérifier la charge utile si elle n'est pas définie",
91
- "it": "controllare il carico utile se non definito",
92
- "es": "check payload si no está definido",
93
- "pl": "sprawdzanie ładunku użytecznego, jeżeli nie określono",
94
- "uk": "перевірити навантаження, якщо не визначено",
95
- "zh-cn": "检查未定义的有效载荷"
96
57
  }
97
58
  },
98
59
  "messages": [
@@ -4,6 +4,7 @@ const utils = require('./utils');
4
4
  const colors = require('./colors.js');
5
5
  const rgb = require('./rgb.js');
6
6
  const ImageController = require('./imageController').ImageController;
7
+ const dmUtils = require('@iobroker/dm-utils');
7
8
 
8
9
  /**
9
10
  * Prüft ob eine ieee_address in der kommaseparierten debugDevices-Liste enthalten ist.
@@ -22,7 +23,7 @@ function isDebugDevice(debugDevices, address) {
22
23
  * Verwaltet das Erstellen und Aktualisieren von Geräte- und Gruppenobjekten in ioBroker
23
24
  * basierend auf den Zigbee2MQTT-Exposes.
24
25
  */
25
- class DeviceController {
26
+ class DeviceController extends dmUtils.DeviceManagement {
26
27
  /**
27
28
  * Erstellt eine neue DeviceController-Instanz.
28
29
  *
@@ -34,6 +35,7 @@ class DeviceController {
34
35
  * @param {object} createCache Cache bereits erstellter ioBroker-Objekte
35
36
  */
36
37
  constructor(adapter, deviceCache, groupCache, config, logCustomizations, createCache) {
38
+ super(adapter);
37
39
  this.adapter = adapter;
38
40
  this.groupCache = groupCache;
39
41
  this.deviceCache = deviceCache;
@@ -75,6 +77,12 @@ class DeviceController {
75
77
  try {
76
78
  const newDevice = await createDeviceFromExposes(devicesMessag, this.adapter);
77
79
  newDevice.icon = await this.imageController.getDeviceIcon(devicesMessag);
80
+ newDevice.manufacturer = devicesMessag.definition.vendor || '';
81
+ newDevice.model = devicesMessag.definition.model || '';
82
+ newDevice.modelDescription = devicesMessag.definition.description || '';
83
+ newDevice.powerSource = devicesMessag.power_source || '';
84
+ newDevice.interviewCompleted = !!devicesMessag.interview_completed;
85
+ newDevice.supported = !!devicesMessag.supported;
78
86
  this.deviceCache.push(newDevice);
79
87
  } catch (err) {
80
88
  // Fix 2: friendly_name im Fehlerlog verwenden
@@ -506,7 +514,7 @@ class DeviceController {
506
514
  }
507
515
 
508
516
  /**
509
- * Gibt den Anzeigenamen eines Geräts zurück.
517
+ * Liefert den Anzeigenamen eines Geräts zurück.
510
518
  * Ist friendly_name gleich der IEEE-Adresse, wird ein leerer String zurückgegeben.
511
519
  *
512
520
  * @param {object} device Das Geräteobjekt aus dem Cache
@@ -526,6 +534,321 @@ class DeviceController {
526
534
  return device.description ? device.description : '';
527
535
  }
528
536
 
537
+ /**
538
+ * Lädt alle Zigbee2MQTT-Geräte und Gruppen und meldet sie an den Device-Manager.
539
+ * Wird vom dm-utils-Framework bei 'dm:loadDevices' aufgerufen.
540
+ *
541
+ * @param {object} context - Der DeviceLoadContext (addDevice / setTotalDevices / complete).
542
+ */
543
+ async loadDevices(context) {
544
+ const allDevices = [...this.deviceCache];
545
+ context.setTotalDevices(allDevices.length);
546
+
547
+ const SKIP_STATES = new Set([
548
+ 'available', 'transition', 'brightness_move', 'brightness_step',
549
+ 'state_toggle', 'effect', 'color_temp_move',
550
+ ]);
551
+
552
+ const SKIP_READ_STATES = new Set([
553
+ 'link_quality', 'simulated_brightness', 'last_seen',
554
+ 'trigger_count', 'trigger_indicator',
555
+ ]);
556
+
557
+ const SKIP_NAME_PREFIXES = [
558
+ 'Indicates how many',
559
+ 'Indicates whether',
560
+ 'Triggered action',
561
+ ];
562
+
563
+ for (const device of allDevices) {
564
+ const status = {};
565
+
566
+ try {
567
+ const availState = await this.adapter.getStateAsync(`${device.ieee_address}.available`);
568
+ status.connection = availState && availState.val === true ? 'connected' : 'disconnected';
569
+ } catch (_e) {
570
+ status.connection = 'disconnected';
571
+ }
572
+
573
+ try {
574
+ const battState = await this.adapter.getStateAsync(`${device.ieee_address}.battery`);
575
+ if (battState && battState.val != null) {
576
+ status.battery = battState.val;
577
+ }
578
+ } catch (_e) { /* kein Batterie-State */ }
579
+
580
+ const displayName = device.id !== device.ieee_address ? device.id : device.ieee_address;
581
+
582
+ const res = {
583
+ id: device.ieee_address,
584
+ name: displayName,
585
+ icon: device.icon || undefined,
586
+ manufacturer: device.manufacturer || '',
587
+ model: `${device.model || ''} ${device.modelDescription || ''}`.trim(),
588
+ status,
589
+ hasDetails: true,
590
+ actions: [],
591
+ };
592
+
593
+ if (Array.isArray(device.states)) {
594
+ const customItems = {}; // kurze Labels → Kachel
595
+ const detailItems = {}; // lange Labels → Detail-Tab
596
+ const writeItems = {};
597
+ const scheduleItems = {};
598
+
599
+ // Maximale Label-Länge für die Kachel ("Sum of produced energy" = 22 Zeichen)
600
+ const MAX_CARD_LABEL_LEN = 30;
601
+
602
+ for (const state of device.states) {
603
+ if (!state || !state.id) { continue; }
604
+ if (SKIP_STATES.has(state.id)) { continue; }
605
+ if (state.isEvent === true) { continue; }
606
+
607
+ const stateId = `${this.adapter.namespace}.${device.ieee_address}.${state.id}`;
608
+ const label = state.name || state.id;
609
+ const unit = state.unit || undefined;
610
+
611
+ const isSchedule = state.id.toLowerCase().includes('schedule') ||
612
+ (state.name || '').toLowerCase().includes('schedule');
613
+
614
+ if (state.write) {
615
+ if (isSchedule) {
616
+ scheduleItems[state.id] = {
617
+ type: 'state', oid: stateId, foreign: true,
618
+ label, newLine: true, minRows: 3, maxRows: 20,
619
+ };
620
+ } else if (state.type === 'boolean') {
621
+ writeItems[state.id] = {
622
+ type: 'state', oid: stateId, foreign: true,
623
+ label, control: 'switch',
624
+ trueText: 'ON', falseText: 'OFF',
625
+ newLine: true,
626
+ };
627
+ } else if (state.type === 'number' && state.min != null && state.max != null) {
628
+ writeItems[state.id] = {
629
+ type: 'state', oid: stateId, foreign: true,
630
+ label, unit, control: 'slider',
631
+ min: state.min, max: state.max, newLine: true,
632
+ };
633
+ } else if (state.states) {
634
+ writeItems[state.id] = {
635
+ type: 'state', oid: stateId, foreign: true,
636
+ label, unit, control: 'select',
637
+ states: state.states, newLine: true,
638
+ };
639
+ } else {
640
+ writeItems[state.id] = {
641
+ type: 'state', oid: stateId, foreign: true,
642
+ label, unit, newLine: true,
643
+ };
644
+ }
645
+ } else if (state.read) {
646
+ if (SKIP_READ_STATES.has(state.id)) { continue; }
647
+ if (isSchedule) { continue; }
648
+ const stateName = state.name || '';
649
+ if (SKIP_NAME_PREFIXES.some(p => stateName.startsWith(p))) { continue; }
650
+
651
+ // Duplikat vermeiden: State bereits über write-Zweig erfasst → überspringen
652
+ if (writeItems[state.id]) { continue; }
653
+
654
+ // LoRaWAN-Muster: oid + foreign:true, Framework liest Live-Wert direkt
655
+ const item = {
656
+ type: 'state',
657
+ oid: stateId,
658
+ foreign: true,
659
+ label,
660
+ unit: unit ?? undefined,
661
+ ...(state.type === 'boolean' ? {
662
+ trueText: '✔',
663
+ falseText: '✘',
664
+ } : {}),
665
+ };
666
+
667
+ if (label.length > MAX_CARD_LABEL_LEN) {
668
+ detailItems[state.id] = item;
669
+ } else {
670
+ customItems[state.id] = item;
671
+ }
672
+ }
673
+ }
674
+
675
+ // Items alphabetisch sortieren (wie LoRaWAN)
676
+ if (Object.keys(customItems).length > 0) {
677
+ const sortedItems = Object.keys(customItems)
678
+ .sort((a, b) => a.localeCompare(b))
679
+ .reduce((acc, key) => { acc[key] = customItems[key]; return acc; }, {});
680
+
681
+ res.customInfo = {
682
+ id: device.ieee_address,
683
+ schema: { type: 'panel', items: sortedItems },
684
+ };
685
+ }
686
+
687
+ device._scheduleItems = scheduleItems;
688
+ device._detailItems = detailItems;
689
+
690
+ if (Object.keys(writeItems).length > 0) {
691
+ res.actions.push({
692
+ id: 'control',
693
+ icon: 'settings',
694
+ description: 'Control device',
695
+ handler: async (_id, ctx) => {
696
+ await ctx.showForm(
697
+ { type: 'panel', label: displayName, items: writeItems },
698
+ { title: `Control – ${displayName}` },
699
+ );
700
+ return { refresh: false };
701
+ },
702
+ });
703
+ }
704
+ }
705
+
706
+ context.addDevice(res);
707
+ }
708
+
709
+ context.complete();
710
+ }
711
+
712
+ /**
713
+ * Gibt Schema und Daten für die Detailansicht eines einzelnen Geräts zurück.
714
+ *
715
+ * @param {string} id Die IEEE-Adresse des Geräts
716
+ * @param {object} _action Das Action-Objekt vom dm-utils-Framework
717
+ * @param {object} _context Der Device-Management-Kontext
718
+ */
719
+ async getDeviceDetails(id, _action, _context) {
720
+ this.adapter.log.debug(`getDeviceDetails: ${id}`);
721
+
722
+ const device = this.deviceCache.find((d) => d.ieee_address === id);
723
+ if (!device) { return null; }
724
+
725
+ // Zeitstempel formatieren (wie LoRaWAN)
726
+ const formatTs = (val) => {
727
+ if (!val) { return '—'; }
728
+ const ts = isNaN(Number(val)) ? new Date(val) : new Date(Number(val));
729
+ if (isNaN(ts.getTime())) { return String(val); }
730
+ return ts.toLocaleString('de-DE', {
731
+ weekday: 'long', year: 'numeric', month: '2-digit', day: '2-digit',
732
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
733
+ });
734
+ };
735
+
736
+ // Live-States lesen
737
+ let lastSeenText = '—';
738
+ let linkQualityText = '—';
739
+ let batteryText = '—';
740
+ let availableText = '—';
741
+
742
+ try {
743
+ const s = await this.adapter.getStateAsync(`${device.ieee_address}.last_seen`);
744
+ if (s && s.val != null && s.val !== '') { lastSeenText = formatTs(s.val); }
745
+ } catch (_e) { /* ignorieren */ }
746
+
747
+ try {
748
+ const s = await this.adapter.getStateAsync(`${device.ieee_address}.link_quality`);
749
+ if (s && s.val != null) { linkQualityText = `${s.val} / 255`; }
750
+ } catch (_e) { /* ignorieren */ }
751
+
752
+ try {
753
+ const s = await this.adapter.getStateAsync(`${device.ieee_address}.battery`);
754
+ if (s && s.val != null) { batteryText = `${s.val} %`; }
755
+ } catch (_e) { /* ignorieren */ }
756
+
757
+ try {
758
+ const s = await this.adapter.getStateAsync(`${device.ieee_address}.available`);
759
+ if (s) { availableText = s.val === true ? '✔ Online' : '✘ Offline'; }
760
+ } catch (_e) { /* ignorieren */ }
761
+
762
+ const displayName = device.id !== device.ieee_address ? device.id : device.ieee_address;
763
+ const scheduleItems = device._scheduleItems || {};
764
+ const hasSchedule = Object.keys(scheduleItems).length > 0;
765
+ const detailItems = device._detailItems || {};
766
+ const hasDetails = Object.keys(detailItems).length > 0;
767
+
768
+ /** @type {Record<string, any>} */
769
+ const data = {};
770
+
771
+ // ── Tab 1: Gerät ─────────────────────────────────────────────────────
772
+ /** @type {Record<string, any>} */
773
+ const infoItems = {};
774
+ infoItems['_h1'] = { type: 'header', text: 'Device Identity', size: 4, newLine: true };
775
+ infoItems['_d1'] = { type: 'divider', color: 'primary' };
776
+ infoItems['ieee'] = { type: 'staticInfo', label: 'IEEE Address', data: device.ieee_address, size: 16, addColon: true, newLine: true };
777
+ infoItems['fname'] = { type: 'staticInfo', label: 'Friendly Name', data: device.id, size: 16, addColon: true, newLine: true };
778
+ infoItems['mfr'] = { type: 'staticInfo', label: 'Manufacturer', data: device.manufacturer || '—', size: 16, addColon: true, newLine: true };
779
+ infoItems['model'] = { type: 'staticInfo', label: 'Model', data: device.model || '—', size: 16, addColon: true, newLine: true };
780
+ infoItems['descr'] = { type: 'staticInfo', label: 'Description', data: device.modelDescription || '—', size: 16, addColon: true, newLine: true };
781
+ infoItems['pwr'] = { type: 'staticInfo', label: 'Power Source', data: device.powerSource || '—', size: 16, addColon: true, newLine: true };
782
+
783
+ // ── Tab 2: Verbindung ─────────────────────────────────────────────────
784
+ /** @type {Record<string, any>} */
785
+ const connItems = {};
786
+ connItems['_h1'] = { type: 'header', text: 'Zigbee Connection', size: 4, newLine: true };
787
+ connItems['_d1'] = { type: 'divider', color: 'primary' };
788
+ connItems['avail'] = { type: 'staticInfo', label: 'Status', data: availableText, size: 16, addColon: true, newLine: true };
789
+ connItems['lq'] = { type: 'staticInfo', label: 'Link Quality', data: linkQualityText, size: 16, addColon: true, newLine: true };
790
+ connItems['ls'] = { type: 'staticInfo', label: 'Last seen', data: lastSeenText, size: 16, addColon: true, newLine: true };
791
+ if (batteryText !== '—') {
792
+ connItems['batt'] = { type: 'staticInfo', label: 'Battery', data: batteryText, size: 16, addColon: true, newLine: true };
793
+ }
794
+
795
+ // ── Tab 3: Technisch ──────────────────────────────────────────────────
796
+ /** @type {Record<string, any>} */
797
+ const techItems = {};
798
+ techItems['_h1'] = { type: 'header', text: 'Zigbee Status', size: 4, newLine: true };
799
+ techItems['_d1'] = { type: 'divider', color: 'primary' };
800
+ techItems['interviewCompleted'] = { type: 'checkbox', label: 'Interview completed', readOnly: true, newLine: true };
801
+ techItems['supported'] = { type: 'checkbox', label: 'Supported by Z2M', readOnly: true, newLine: true };
802
+ techItems['disabled'] = { type: 'checkbox', label: 'Disabled', readOnly: true, newLine: true };
803
+
804
+ data.interviewCompleted = !!device.interviewCompleted;
805
+ data.supported = !!device.supported;
806
+ data.disabled = !!device.disabled;
807
+
808
+ // ── Tabs zusammenbauen ────────────────────────────────────────────────
809
+ /** @type {Record<string, any>} */
810
+ const tabs = {
811
+ _tab_info: { type: 'panel', label: 'Device', items: infoItems },
812
+ _tab_conn: { type: 'panel', label: 'Connection', items: connItems },
813
+ _tab_tech: { type: 'panel', label: 'Technical', items: techItems },
814
+ };
815
+
816
+ // Values-Tab: States mit langen Labels
817
+ if (hasDetails) {
818
+ const sortedDetailItems = Object.keys(detailItems)
819
+ .sort((a, b) => a.localeCompare(b))
820
+ .reduce((acc, key) => { acc[key] = detailItems[key]; return acc; }, {});
821
+ tabs._tab_values = {
822
+ type: 'panel',
823
+ label: 'Values',
824
+ items: {
825
+ _h1: { type: 'header', text: 'Additional Values', size: 4, newLine: true },
826
+ _d1: { type: 'divider', color: 'primary' },
827
+ ...sortedDetailItems,
828
+ },
829
+ };
830
+ }
831
+
832
+ // Schedule-Tab: nur für Thermostate
833
+ if (hasSchedule) {
834
+ tabs._tab_schedule = {
835
+ type: 'panel',
836
+ label: 'Schedule',
837
+ items: {
838
+ _h1: { type: 'header', text: 'Weekly Schedule', size: 4, newLine: true },
839
+ _d1: { type: 'divider', color: 'primary' },
840
+ ...scheduleItems,
841
+ },
842
+ };
843
+ }
844
+
845
+ return {
846
+ id: device.ieee_address,
847
+ schema: { type: 'tabs', items: tabs },
848
+ data,
849
+ };
850
+ }
851
+
529
852
  /**
530
853
  * Verarbeitet die Antwort eines Coordinator-Check-Requests von Zigbee2MQTT
531
854
  * und schreibt fehlende Router in die info-States.
@@ -535,8 +858,8 @@ class DeviceController {
535
858
  async processCoordinatorCheck(payload) {
536
859
  if (payload && payload.data && payload.data.missing_routers) {
537
860
  const missingRoutersCount = payload.data.missing_routers.length;
538
- await this.adapter.setStateAsync('info.missing_routers', JSON.stringify(payload.data.missing_routers), true);
539
- await this.adapter.setStateAsync('info.missing_routers_count', missingRoutersCount, true);
861
+ this.adapter.setState('info.missing_routers', JSON.stringify(payload.data.missing_routers), true);
862
+ this.adapter.setState('info.missing_routers_count', missingRoutersCount, true);
540
863
 
541
864
  if (missingRoutersCount > 0) {
542
865
  const logLvl = this.config.coordinatorCheckLogLvl;