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 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
@@ -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,21 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "zigbee2mqtt",
4
- "version": "3.2.0",
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": [
@@ -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.
package/lib/exposes.js CHANGED
@@ -726,29 +726,26 @@ async function createDeviceFromExposes(devicesMessag, adapter) {
726
726
  break;
727
727
 
728
728
  case 'illuminance':
729
- // Z2M sendet 'illuminance' = Rohwert (kein Lux)
730
- // Wir legen BEIDE States an:
731
- // 1. illuminance_raw – der unveränderte Rohwert
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 direkt (eigener Expose-Typ)
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.illuminance_from_illuminance_raw;
737
+ state = statesDefs.illuminance_from_raw;
746
738
  break;
747
739
 
748
740
  case 'illuminance_lux':
749
- state = statesDefs.illuminance;
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 nedb-Persistenz (aedes + aedes-persistence-nedb).
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 NedbPersistence = require('aedes-persistence-nedb');
63
- const db = new NedbPersistence({
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
- * Illuminance berechnet aus dem Z2M-Rohwert (prop='illuminance').
351
- * Formel: lux = round(10^(raw / 10000))
352
- * Wird zusätzlich zu illuminance_raw angelegt wenn das Gerät keinen
353
- * illuminance_lux-Expose liefert.
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
- illuminance_from_raw: {
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
- illuminance_raw: {
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 wenn das Gerät 'illuminance_raw' als payload-Schlüssel sendet
389
- * (z.B. Geräte mit eigenem illuminance_raw-Expose).
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
- * Illuminance in Lux berechnet aus payload.illuminance_raw.
406
- * Formel: lux = round(10^(illuminance_raw / 10000))
407
- * Wird angelegt wenn das Gerät 'illuminance_raw' als Expose-Typ hat,
408
- * aber kein separates 'illuminance_lux' liefert.
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
- illuminance_from_illuminance_raw: {
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
- if (payload.illuminance_raw == null) { return undefined; }
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.0",
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": ">= 20"
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-nedb": "^2.0.3",
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.2"
49
+ "typescript": "~6.0.3"
50
50
  },
51
51
  "main": "main.js",
52
52
  "files": [