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 +12 -80
- package/admin/jsonConfig.json +10 -1
- package/io-package.json +40 -79
- package/lib/deviceController.js +327 -4
- package/lib/exposes.js +1359 -1362
- package/lib/mqttServerController.js +28 -34
- package/lib/states.js +93 -19
- package/lib/statesController.js +4 -4
- package/lib/websocketController.js +1 -1
- package/lib/z2mController.js +3 -3
- package/main.js +26 -17
- package/package.json +7 -7
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
|
|
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.
|
|
29
|
-
* (arteck)
|
|
31
|
+
### 3.2.0 (2026-04-26)
|
|
32
|
+
* (arteck) del deprectated setStateAsync
|
|
30
33
|
|
|
31
|
-
### 3.1.
|
|
32
|
-
* (arteck) fix
|
|
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
|
-
###
|
|
37
|
+
### 3.1.8 (2026-04-26)
|
|
38
|
+
* (arteck) back to old dependency aedes
|
|
108
39
|
|
|
109
|
-
-
|
|
40
|
+
### 3.1.7 (2026-04-24)
|
|
41
|
+
* (arteck) fix illuminance = 0
|
|
110
42
|
|
|
111
43
|
## License
|
|
112
44
|
|
package/admin/jsonConfig.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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": [
|
package/lib/deviceController.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
539
|
-
|
|
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;
|