iobroker.zigbee2mqtt 3.2.0 → 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 +6 -3
- package/admin/jsonConfig.json +10 -1
- package/io-package.json +14 -53
- package/lib/deviceController.js +325 -2
- package/lib/exposes.js +11 -14
- package/lib/mqttServerController.js +6 -12
- package/lib/states.js +90 -20
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -22,6 +22,12 @@ 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.2.1 (2026-05-05)
|
|
26
|
+
* (copilot) Adapter requires node.js >= 22 now
|
|
27
|
+
* (arteck) upd device manager
|
|
28
|
+
* (arteck) fix aedes-persistence
|
|
29
|
+
* (arteck) fix illuminance
|
|
30
|
+
|
|
25
31
|
### 3.2.0 (2026-04-26)
|
|
26
32
|
* (arteck) del deprectated setStateAsync
|
|
27
33
|
|
|
@@ -34,9 +40,6 @@ This adapter allows to control the data points of the devices of a Zigbee2MQTT i
|
|
|
34
40
|
### 3.1.7 (2026-04-24)
|
|
35
41
|
* (arteck) fix illuminance = 0
|
|
36
42
|
|
|
37
|
-
### 3.1.5 (2026-04-23)
|
|
38
|
-
* (arteck) fix illuminance
|
|
39
|
-
|
|
40
43
|
## License
|
|
41
44
|
|
|
42
45
|
MIT License
|
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,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "zigbee2mqtt",
|
|
4
|
-
"version": "3.2.
|
|
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
|
+
},
|
|
6
19
|
"3.2.0": {
|
|
7
20
|
"en": "del deprectated setStateAsync",
|
|
8
21
|
"de": "del deprected setStateAsync",
|
|
@@ -29,45 +42,6 @@
|
|
|
29
42
|
"uk": "застібка WS",
|
|
30
43
|
"zh-cn": "修复 WS 关闭"
|
|
31
44
|
},
|
|
32
|
-
"3.1.8": {
|
|
33
|
-
"en": "back to old dependency aedes",
|
|
34
|
-
"de": "zurück zu den alten abhängigkeiten",
|
|
35
|
-
"ru": "возвращение к старой зависимости",
|
|
36
|
-
"pt": "voltar ao velho aedes dependência",
|
|
37
|
-
"nl": "terug naar oude afhankelijkheid",
|
|
38
|
-
"fr": "retour à de vieilles dépendances",
|
|
39
|
-
"it": "torna alla vecchia dipendenza",
|
|
40
|
-
"es": "volver a las viejas",
|
|
41
|
-
"pl": "powrót do dawnej zależności",
|
|
42
|
-
"uk": "назад до старих залежностей",
|
|
43
|
-
"zh-cn": "回到旧的依附物"
|
|
44
|
-
},
|
|
45
|
-
"3.1.7": {
|
|
46
|
-
"en": "fix illuminance = 0",
|
|
47
|
-
"de": "fixe beleuchtungsstärke = 0",
|
|
48
|
-
"ru": "фиксированная освещенность = 0",
|
|
49
|
-
"pt": "iluminação de fixação = 0",
|
|
50
|
-
"nl": "fix verlichtingssterkte = 0",
|
|
51
|
-
"fr": "fixer l'éclairage = 0",
|
|
52
|
-
"it": "illuminazione di correzione = 0",
|
|
53
|
-
"es": "iluminación de fijación = 0",
|
|
54
|
-
"pl": "podświetlenie = 0",
|
|
55
|
-
"uk": "фіксувати ілюмінанс = 0",
|
|
56
|
-
"zh-cn": "固定灯光=0"
|
|
57
|
-
},
|
|
58
|
-
"3.1.6": {
|
|
59
|
-
"en": "fix illuminance = 0",
|
|
60
|
-
"de": "fixe beleuchtungsstärke = 0",
|
|
61
|
-
"ru": "фиксированная освещенность = 0",
|
|
62
|
-
"pt": "iluminação de fixação = 0",
|
|
63
|
-
"nl": "fix verlichtingssterkte = 0",
|
|
64
|
-
"fr": "fixer l'éclairage = 0",
|
|
65
|
-
"it": "illuminazione di correzione = 0",
|
|
66
|
-
"es": "iluminación de fijación = 0",
|
|
67
|
-
"pl": "podświetlenie = 0",
|
|
68
|
-
"uk": "фіксувати ілюмінанс = 0",
|
|
69
|
-
"zh-cn": "固定灯光=0"
|
|
70
|
-
},
|
|
71
45
|
"3.1.5": {
|
|
72
46
|
"en": "fix illuminance",
|
|
73
47
|
"de": "befestigungsleuchten",
|
|
@@ -80,19 +54,6 @@
|
|
|
80
54
|
"pl": "podświetlenie",
|
|
81
55
|
"uk": "фіксувати ілюмінанс",
|
|
82
56
|
"zh-cn": "修补灯光"
|
|
83
|
-
},
|
|
84
|
-
"3.1.4": {
|
|
85
|
-
"en": "fix illuminance if not exists. we calculate it ourselves",
|
|
86
|
-
"de": "fixe beleuchtungsstärke, wenn nicht vorhanden. wir berechnen es selbst",
|
|
87
|
-
"ru": "установить свет, если его нет. мы сами вычисляем",
|
|
88
|
-
"pt": "fixar a iluminação se não existir. nós mesmos calculamos",
|
|
89
|
-
"nl": "fix verlichtingssterkte indien niet bestaat. we berekenen het zelf",
|
|
90
|
-
"fr": "fixer l'éclairage s'il n'existe pas. nous le calculons nous-mêmes",
|
|
91
|
-
"it": "fissare l'illuminazione se non esiste. noi lo calcoliamo noi stessi",
|
|
92
|
-
"es": "fijar la iluminación si no existe. lo calculamos nosotros mismos",
|
|
93
|
-
"pl": "naprawić oświetlenie, jeśli nie istnieje. sami to obliczymy",
|
|
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.
|
package/lib/exposes.js
CHANGED
|
@@ -726,29 +726,26 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
|
|
|
726
726
|
break;
|
|
727
727
|
|
|
728
728
|
case 'illuminance':
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
// 2. illuminance – in Lux konvertiert (10^(raw/10000))
|
|
733
|
-
// Damit bleiben bestehende Scripts auf illuminance_raw kompatibel
|
|
734
|
-
// und neue Scripts können direkt illuminance (Lux) nutzen.
|
|
735
|
-
pushToStates(statesDefs.illuminance_raw, expose.access);
|
|
736
|
-
state = statesDefs.illuminance_from_raw;
|
|
729
|
+
// Gerät sendet payload.illuminance (Lux in neuem Z2M).
|
|
730
|
+
pushToStates(statesDefs.illuminance_direct, expose.access);
|
|
731
|
+
state = statesDefs.illuminance_raw_from_lux;
|
|
737
732
|
break;
|
|
738
733
|
|
|
739
734
|
case 'illuminance_raw':
|
|
740
|
-
// Gerät sendet payload.illuminance_raw
|
|
741
|
-
// Wir legen ebenfalls BEIDE States an:
|
|
742
|
-
// 1. illuminance_raw – der unveränderte Rohwert (prop='illuminance_raw')
|
|
743
|
-
// 2. illuminance – in Lux konvertiert (10^(raw/10000))
|
|
735
|
+
// Gerät sendet payload.illuminance_raw (ZCL-Rohwert).
|
|
744
736
|
pushToStates(statesDefs.illuminance_raw_direct, expose.access);
|
|
745
|
-
state = statesDefs.
|
|
737
|
+
state = statesDefs.illuminance_from_raw;
|
|
746
738
|
break;
|
|
747
739
|
|
|
748
740
|
case 'illuminance_lux':
|
|
749
|
-
|
|
741
|
+
// Geräte wie Philips HUE Motion, IKEA liefern illuminance_lux (bereits Lux).
|
|
742
|
+
// illuminance_lux_direct: übernimmt Lux direkt.
|
|
743
|
+
// illuminance_raw_from_lux2: berechnet Rohwert – nur wenn kein illuminance_raw im Payload.
|
|
744
|
+
pushToStates(statesDefs.illuminance_lux_direct, expose.access);
|
|
745
|
+
state = statesDefs.illuminance_raw_from_lux2;
|
|
750
746
|
break;
|
|
751
747
|
|
|
748
|
+
|
|
752
749
|
case 'power':
|
|
753
750
|
state = statesDefs.load_power;
|
|
754
751
|
break;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const core = require('@iobroker/adapter-core');
|
|
4
3
|
const Aedes = require('aedes');
|
|
5
4
|
const net = require('node:net');
|
|
6
5
|
|
|
@@ -51,24 +50,19 @@ class MqttServerController {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
|
-
* Startet den internen MQTT-Server mit
|
|
53
|
+
* Startet den internen MQTT-Server mit In-Memory-Persistenz (aedes + aedes-persistence-memory).
|
|
55
54
|
* Wartet auf den erfolgreichen listen()-Aufruf bevor die Methode zurückkehrt.
|
|
55
|
+
* Hinweis: aedes-persistence-nedb wurde entfernt, da es das veraltete nedb-Paket
|
|
56
|
+
* mit util.isDate verwendet (entfernt in Node.js v12+). In-Memory ist für einen
|
|
57
|
+
* internen Broker ausreichend – Clients subscriben sich bei Reconnect automatisch neu.
|
|
56
58
|
*/
|
|
57
59
|
async createMQTTServer() {
|
|
58
60
|
if (!this._checkConfig()) {
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
try {
|
|
62
|
-
const
|
|
63
|
-
const db =
|
|
64
|
-
path: `${core.getAbsoluteInstanceDataDir(this.adapter)}/mqttData`,
|
|
65
|
-
prefix: '',
|
|
66
|
-
});
|
|
67
|
-
// aedes-persistence-nedb does not implement the async setup() interface
|
|
68
|
-
// required by aedes v1.x → no-op shim
|
|
69
|
-
if (!db.setup) {
|
|
70
|
-
db.setup = async () => {};
|
|
71
|
-
}
|
|
64
|
+
const MemoryPersistence = require('aedes-persistence');
|
|
65
|
+
const db = MemoryPersistence();
|
|
72
66
|
const aedes = Aedes({ persistence: db });
|
|
73
67
|
this.aedesBroker = aedes;
|
|
74
68
|
this.mqttServer = net.createServer(aedes.handle);
|
package/lib/states.js
CHANGED
|
@@ -347,12 +347,19 @@ const states = {
|
|
|
347
347
|
},
|
|
348
348
|
|
|
349
349
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
350
|
+
* illuminance (Lux-State) – ausgelöst durch payload.illuminance.
|
|
351
|
+
*
|
|
352
|
+
* Fall 1 – beide Werte im Payload (illuminance + illuminance_raw):
|
|
353
|
+
* payload.illuminance wird direkt übernommen (neues Z2M sendet bereits Lux).
|
|
354
|
+
*
|
|
355
|
+
* Fall 2 – nur illuminance im Payload:
|
|
356
|
+
* payload.illuminance wird direkt übernommen.
|
|
357
|
+
*
|
|
358
|
+
* Fall 3 – nur illuminance_raw im Payload:
|
|
359
|
+
* Dieser State wird NICHT von diesem Eintrag gesetzt (prop='illuminance' nicht vorhanden).
|
|
360
|
+
* Stattdessen übernimmt illuminance_from_raw (prop='illuminance_raw') die Berechnung.
|
|
354
361
|
*/
|
|
355
|
-
|
|
362
|
+
illuminance_direct: {
|
|
356
363
|
id: 'illuminance',
|
|
357
364
|
prop: 'illuminance',
|
|
358
365
|
name: 'Illuminance',
|
|
@@ -363,15 +370,20 @@ const states = {
|
|
|
363
370
|
type: 'number',
|
|
364
371
|
def: 0,
|
|
365
372
|
unit: 'lux',
|
|
366
|
-
getter: (payload) => {
|
|
367
|
-
if (payload.illuminance == null) { return undefined; }
|
|
368
|
-
const raw = payload.illuminance;
|
|
369
|
-
if (raw <= 0) { return 0; }
|
|
370
|
-
return Math.round(Math.pow(10, (raw - 1) / 10000));
|
|
371
|
-
},
|
|
372
373
|
},
|
|
373
374
|
|
|
374
|
-
|
|
375
|
+
/**
|
|
376
|
+
* illuminance_raw (Rohwert-State) – ausgelöst durch payload.illuminance.
|
|
377
|
+
*
|
|
378
|
+
* Fall 2 – nur illuminance im Payload:
|
|
379
|
+
* illuminance_raw wird aus dem Lux-Wert zurückgerechnet:
|
|
380
|
+
* raw = round(log10(lux) * 10000 + 1)
|
|
381
|
+
*
|
|
382
|
+
* Fall 1 – beide Werte im Payload:
|
|
383
|
+
* Getter gibt undefined zurück → dieser State wird NICHT gesetzt,
|
|
384
|
+
* stattdessen setzt illuminance_raw_direct (prop='illuminance_raw') den Rohwert direkt.
|
|
385
|
+
*/
|
|
386
|
+
illuminance_raw_from_lux: {
|
|
375
387
|
id: 'illuminance_raw',
|
|
376
388
|
prop: 'illuminance',
|
|
377
389
|
name: 'Illuminance raw',
|
|
@@ -382,11 +394,21 @@ const states = {
|
|
|
382
394
|
type: 'number',
|
|
383
395
|
def: 0,
|
|
384
396
|
unit: '',
|
|
397
|
+
getter: (payload) => {
|
|
398
|
+
// Wenn illuminance_raw bereits im Payload → illuminance_raw_direct setzt ihn direkt
|
|
399
|
+
if (payload.illuminance_raw != null) { return undefined; }
|
|
400
|
+
const lux = payload.illuminance;
|
|
401
|
+
if (lux == null) { return undefined; }
|
|
402
|
+
if (lux <= 0) { return 0; }
|
|
403
|
+
return Math.round(Math.log10(lux) * 10000 + 1);
|
|
404
|
+
},
|
|
385
405
|
},
|
|
386
406
|
|
|
387
407
|
/**
|
|
388
|
-
* Rohwert-State
|
|
389
|
-
*
|
|
408
|
+
* illuminance_raw (Rohwert-State) – ausgelöst durch payload.illuminance_raw.
|
|
409
|
+
*
|
|
410
|
+
* Fall 1 – beide Werte im Payload: direkt übernehmen.
|
|
411
|
+
* Fall 3 – nur illuminance_raw im Payload: direkt übernehmen.
|
|
390
412
|
*/
|
|
391
413
|
illuminance_raw_direct: {
|
|
392
414
|
id: 'illuminance_raw',
|
|
@@ -402,12 +424,15 @@ const states = {
|
|
|
402
424
|
},
|
|
403
425
|
|
|
404
426
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
427
|
+
* illuminance (Lux-State) – ausgelöst durch payload.illuminance_raw.
|
|
428
|
+
*
|
|
429
|
+
* Fall 3 – nur illuminance_raw im Payload:
|
|
430
|
+
* Lux wird berechnet: lux = round(10^((raw - 1) / 10000))
|
|
431
|
+
*
|
|
432
|
+
* Fall 1 – beide Werte im Payload:
|
|
433
|
+
* Getter gibt undefined zurück → illuminance_direct (prop='illuminance') setzt Lux direkt.
|
|
409
434
|
*/
|
|
410
|
-
|
|
435
|
+
illuminance_from_raw: {
|
|
411
436
|
id: 'illuminance',
|
|
412
437
|
prop: 'illuminance_raw',
|
|
413
438
|
name: 'Illuminance',
|
|
@@ -419,13 +444,58 @@ const states = {
|
|
|
419
444
|
def: 0,
|
|
420
445
|
unit: 'lux',
|
|
421
446
|
getter: (payload) => {
|
|
422
|
-
|
|
447
|
+
// Wenn illuminance bereits im Payload → illuminance_direct setzt Lux direkt
|
|
448
|
+
if (payload.illuminance != null) { return undefined; }
|
|
423
449
|
const raw = payload.illuminance_raw;
|
|
450
|
+
if (raw == null) { return undefined; }
|
|
424
451
|
if (raw <= 0) { return 0; }
|
|
425
452
|
return Math.round(Math.pow(10, (raw - 1) / 10000));
|
|
426
453
|
},
|
|
427
454
|
},
|
|
428
455
|
|
|
456
|
+
/**
|
|
457
|
+
* illuminance (Lux-State) – ausgelöst durch payload.illuminance_lux.
|
|
458
|
+
* Geräte wie Philips HUE Motion, IKEA liefern illuminance_lux (bereits Lux).
|
|
459
|
+
* Wert wird direkt übernommen.
|
|
460
|
+
*/
|
|
461
|
+
illuminance_lux_direct: {
|
|
462
|
+
id: 'illuminance',
|
|
463
|
+
prop: 'illuminance_lux',
|
|
464
|
+
name: 'Illuminance',
|
|
465
|
+
icon: undefined,
|
|
466
|
+
role: 'value.brightness',
|
|
467
|
+
write: false,
|
|
468
|
+
read: true,
|
|
469
|
+
type: 'number',
|
|
470
|
+
def: 0,
|
|
471
|
+
unit: 'lux',
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* illuminance_raw (Rohwert-State) – ausgelöst durch payload.illuminance_lux.
|
|
476
|
+
* Berechnet: raw = round(log10(lux) * 10000 + 1)
|
|
477
|
+
* NUR wenn kein illuminance_raw im Payload – kein Überschreiben.
|
|
478
|
+
*/
|
|
479
|
+
illuminance_raw_from_lux2: {
|
|
480
|
+
id: 'illuminance_raw',
|
|
481
|
+
prop: 'illuminance_lux',
|
|
482
|
+
name: 'Illuminance raw',
|
|
483
|
+
icon: undefined,
|
|
484
|
+
role: 'value.brightness',
|
|
485
|
+
write: false,
|
|
486
|
+
read: true,
|
|
487
|
+
type: 'number',
|
|
488
|
+
def: 0,
|
|
489
|
+
unit: '',
|
|
490
|
+
getter: (payload) => {
|
|
491
|
+
if (payload.illuminance_raw != null) { return undefined; }
|
|
492
|
+
const lux = payload.illuminance_lux;
|
|
493
|
+
if (lux == null) { return undefined; }
|
|
494
|
+
if (lux <= 0) { return 0; }
|
|
495
|
+
return Math.round(Math.log10(lux) * 10000 + 1);
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
|
|
429
499
|
// Fix: prop ergänzt – Z2M sendet payload.occupancy
|
|
430
500
|
occupancy: {
|
|
431
501
|
id: 'occupancy',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.zigbee2mqtt",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Zigbee2MQTT adapter for ioBroker",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Dennis Rathjen and Arthur Rupp",
|
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
"url": "https://github.com/arteck/ioBroker.zigbee2mqtt.git"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": ">=
|
|
24
|
+
"node": ">= 22"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@iobroker/adapter-core": "^3.3.2",
|
|
28
28
|
"@iobroker/dm-utils": "^3.0.17",
|
|
29
29
|
"aedes": "^0.51.3",
|
|
30
|
-
"aedes-persistence
|
|
30
|
+
"aedes-persistence": "^9.1.2",
|
|
31
|
+
"axios": "^1.15.2",
|
|
31
32
|
"mqtt": "^5.15.1",
|
|
32
|
-
"axios": "^1.14.0",
|
|
33
33
|
"net": "^1.0.2",
|
|
34
34
|
"node-schedule": "^2.1.1",
|
|
35
35
|
"sharp": "^0.34.5",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"@alcalzone/release-script-plugin-license": "^5.1.1",
|
|
42
42
|
"@alcalzone/release-script-plugin-manual-review": "^5.1.1",
|
|
43
43
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
44
|
-
"@iobroker/testing": "^5.2.2",
|
|
45
44
|
"@iobroker/eslint-config": "^2.1.0",
|
|
45
|
+
"@iobroker/testing": "^5.2.2",
|
|
46
46
|
"@tsconfig/node14": "^14.1.8",
|
|
47
47
|
"@types/node": "^25.5.2",
|
|
48
48
|
"@types/node-schedule": "^2.1.8",
|
|
49
|
-
"typescript": "~6.0.
|
|
49
|
+
"typescript": "~6.0.3"
|
|
50
50
|
},
|
|
51
51
|
"main": "main.js",
|
|
52
52
|
"files": [
|