iobroker.utility-monitor 1.4.5 → 1.4.6

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
@@ -61,10 +61,55 @@ Gefällt dir dieser Adapter? Du kannst mich gerne mit einem Kaffee unterstützen
61
61
 
62
62
  ---
63
63
 
64
+ ## ⚠️ Breaking Changes in Version 1.4.6
65
+
66
+ **WICHTIG:** Version 1.4.6 ändert die State-Struktur grundlegend!
67
+
68
+ ### Was hat sich geändert?
69
+
70
+ **Vorher (bis 1.4.5):**
71
+ ```
72
+ gas.consumption.daily
73
+ gas.costs.monthly
74
+ wasser.consumption.daily
75
+ ```
76
+
77
+ **Jetzt (ab 1.4.6):**
78
+ ```
79
+ gas.main.consumption.daily ← Hauptzähler mit Namen "main"
80
+ gas.main.costs.monthly
81
+ wasser.main.consumption.daily
82
+ ```
83
+
84
+ ### 🔧 Migration erforderlich
85
+
86
+ 1. **Config öffnen**: Neue Felder "Name des Hauptzählers" für Gas/Wasser/Strom/PV
87
+ 2. **Namen eingeben**: Standard ist "main" (empfohlen), oder eigener Name wie "wohnung", "haus"
88
+ 3. **Skripte anpassen**: Alle Verweise auf States müssen angepasst werden
89
+ ```javascript
90
+ // Alt:
91
+ getState('utility-monitor.0.gas.consumption.daily')
92
+
93
+ // Neu:
94
+ getState('utility-monitor.0.gas.main.consumption.daily')
95
+ ```
96
+ 4. **Visualisierungen updaten**: VIS, Grafana, etc. auf neue Pfade anpassen
97
+
98
+ ### 💡 Warum diese Änderung?
99
+
100
+ - **Konsistenz**: Alle Zähler (Haupt + Zusätzlich) verwenden jetzt die gleiche Struktur
101
+ - **Flexibilität**: Hauptzähler kann jetzt frei benannt werden (z.B. "erdgeschoss", "gesamt")
102
+ - **Klarheit**: Keine Special-Case Logik mehr im Code
103
+ - **Multi-Meter**: Bessere Unterstützung für mehrere Zähler pro Typ
104
+
105
+ ---
106
+
64
107
  ## 📊 Datenpunkte erklärt
65
108
 
66
109
  Für jede aktivierte Verbrauchsart (Gas/Wasser/Strom/PV) werden folgende Ordner angelegt:
67
110
 
111
+ **Wichtig:** Seit Version 1.4.6 beinhalten alle Pfade den Zähler-Namen (z.B. `gas.main.*` statt `gas.*`)
112
+
68
113
  ### 🗂️ **consumption** (Verbrauch)
69
114
 
70
115
  | Datenpunkt | Beschreibung | Beispiel |
@@ -229,6 +274,33 @@ Der Adapter setzt Zähler automatisch zurück:
229
274
 
230
275
  ## Changelog
231
276
 
277
+ ### 1.4.6 (2026-01-20)
278
+
279
+ - **⚠️ BREAKING CHANGE:** 🔄 **Main Meter Naming** - Hauptzähler benötigt jetzt einen Namen:
280
+ - **State-Pfade geändert**: `gas.*` → `gas.METER_NAME.*` (z.B. `gas.main.*`)
281
+ - **Neue Config-Felder**: "Name des Hauptzählers" für Gas/Wasser/Strom/PV
282
+ - **Default-Name**: "main" (wird automatisch verwendet wenn leer gelassen)
283
+ - **Konsistente Struktur**: Alle Zähler (Haupt + Zusätzlich) verwenden jetzt `type.meterName.*`
284
+ - **Flexibilität**: Hauptzähler kann jetzt frei benannt werden (z.B. "wohnung", "erdgeschoss", "gesamt")
285
+ - **Keine Special-Cases**: Vereinfachter Code ohne `meterName === 'main'` Bedingungen
286
+ - **NEW:** 🔔 **Smart Notifications** - Zählerauswahl für Benachrichtigungen:
287
+ - Wähle pro Utility-Typ aus, welche Zähler benachrichtigt werden sollen
288
+ - Multi-Select Dropdown zeigt alle konfigurierten Zähler
289
+ - Wenn leer: Alle Zähler werden benachrichtigt (Standard)
290
+ - Wenn ausgewählt: Nur gewählte Zähler erhalten Benachrichtigungen
291
+ - Gilt für Abrechnungsende, Vertragswechsel und monatliche Berichte
292
+ - **IMPROVED:** 🏗️ **Code-Architektur** - Entfernung von 19 Special-Case Checks in 7 Dateien:
293
+ - Vereinfachte basePath-Berechnungen in multiMeterManager, billingManager, stateManager
294
+ - Vereinheitlichter Config-Zugriff (alle Meter nutzen `meter.config.contractStart`)
295
+ - HT/NT-Logik basiert jetzt auf `config.htNtEnabled` statt Meter-Name
296
+ - Button-Trigger erkennt nur noch einheitliche Pfadstruktur
297
+ - Legacy-Code entfernt: updateBillingCountdown, updateCurrentPrice jetzt per-meter
298
+ - **MIGRATION:** 📋 **Upgrade-Hinweise**:
299
+ - Bei Neuinstallation: Namen für Hauptzähler eingeben (oder "main" akzeptieren)
300
+ - Bei Upgrade: Adapter neu konfigurieren + Skripte/Visualisierungen anpassen
301
+ - History: Alte States bleiben erhalten, neue States werden parallel erstellt
302
+ - Empfehlung: "main" als Namen verwenden für einfachere Migration
303
+
232
304
  ### 1.4.5 (2026-01-20)
233
305
 
234
306
  - **FIX:** 🐛 **Critical Multi-Meter Cost Calculation Bugs** - Comprehensive fixes for multi-meter functionality:
@@ -21,6 +21,19 @@
21
21
  "lg": 6,
22
22
  "xl": 6
23
23
  },
24
+ "gasMainMeterName": {
25
+ "type": "text",
26
+ "label": "📝 Name des Hauptzählers",
27
+ "placeholder": "z.B. main, wohnung, haus, gesamt",
28
+ "help": "Technischer Name für den Hauptzähler. Wird Teil des State-Pfads (gas.DIESER_NAME.*). Nur Kleinbuchstaben und Zahlen, max 20 Zeichen.",
29
+ "default": "main",
30
+ "hidden": "!data.gasAktiv",
31
+ "sm": 12,
32
+ "md": 6,
33
+ "xs": 12,
34
+ "lg": 6,
35
+ "xl": 4
36
+ },
24
37
  "_gasSensorHeader": {
25
38
  "type": "header",
26
39
  "text": "Sensor-Konfiguration",
@@ -451,6 +464,19 @@
451
464
  "lg": 6,
452
465
  "xl": 6
453
466
  },
467
+ "wasserMainMeterName": {
468
+ "type": "text",
469
+ "label": "📝 Name des Hauptzählers",
470
+ "placeholder": "z.B. main, wohnung, haus, gesamt",
471
+ "help": "Technischer Name für den Hauptzähler. Wird Teil des State-Pfads (wasser.DIESER_NAME.*). Nur Kleinbuchstaben und Zahlen, max 20 Zeichen.",
472
+ "default": "main",
473
+ "hidden": "!data.wasserAktiv",
474
+ "sm": 12,
475
+ "md": 6,
476
+ "xs": 12,
477
+ "lg": 6,
478
+ "xl": 4
479
+ },
454
480
  "_wasserSensorHeader": {
455
481
  "type": "header",
456
482
  "text": "Sensor-Konfiguration",
@@ -745,6 +771,19 @@
745
771
  "lg": 6,
746
772
  "xl": 6
747
773
  },
774
+ "stromMainMeterName": {
775
+ "type": "text",
776
+ "label": "📝 Name des Hauptzählers",
777
+ "placeholder": "z.B. main, wohnung, haus, gesamt",
778
+ "help": "Technischer Name für den Hauptzähler. Wird Teil des State-Pfads (strom.DIESER_NAME.*). Nur Kleinbuchstaben und Zahlen, max 20 Zeichen.",
779
+ "default": "main",
780
+ "hidden": "!data.stromAktiv",
781
+ "sm": 12,
782
+ "md": 6,
783
+ "xs": 12,
784
+ "lg": 6,
785
+ "xl": 4
786
+ },
748
787
  "_stromSensorHeader": {
749
788
  "type": "header",
750
789
  "text": "Sensor-Konfiguration",
@@ -1132,6 +1171,19 @@
1132
1171
  "lg": 6,
1133
1172
  "xl": 6
1134
1173
  },
1174
+ "pvMainMeterName": {
1175
+ "type": "text",
1176
+ "label": "📝 Name des Hauptzählers",
1177
+ "placeholder": "z.B. main, einspeisung, pv",
1178
+ "help": "Technischer Name für den Hauptzähler. Wird Teil des State-Pfads (pv.DIESER_NAME.*). Nur Kleinbuchstaben und Zahlen, max 20 Zeichen.",
1179
+ "default": "main",
1180
+ "hidden": "!data.pvAktiv",
1181
+ "sm": 12,
1182
+ "md": 6,
1183
+ "xs": 12,
1184
+ "lg": 6,
1185
+ "xl": 4
1186
+ },
1135
1187
  "_pvSensorHeader": {
1136
1188
  "type": "header",
1137
1189
  "text": "Sensor-Konfiguration",
@@ -1423,6 +1475,20 @@
1423
1475
  "lg": 4,
1424
1476
  "xl": 4
1425
1477
  },
1478
+ "notificationGasMeters": {
1479
+ "type": "selectSendTo",
1480
+ "label": "Welche Gas-Zähler benachrichtigen?",
1481
+ "command": "getMeters",
1482
+ "jsonData": "{\"type\": \"gas\"}",
1483
+ "multiple": true,
1484
+ "help": "Wähle die Zähler aus, für die du Benachrichtigungen erhalten möchtest. Wenn leer, werden ALLE Zähler benachrichtigt.",
1485
+ "hidden": "!data.notificationEnabled || !data.notificationGasEnabled || !data.gasAktiv",
1486
+ "sm": 12,
1487
+ "md": 8,
1488
+ "xs": 12,
1489
+ "lg": 8,
1490
+ "xl": 8
1491
+ },
1426
1492
  "notificationWasserEnabled": {
1427
1493
  "type": "checkbox",
1428
1494
  "label": "Wasser-Erinnerung",
@@ -1433,6 +1499,20 @@
1433
1499
  "lg": 4,
1434
1500
  "xl": 4
1435
1501
  },
1502
+ "notificationWasserMeters": {
1503
+ "type": "selectSendTo",
1504
+ "label": "Welche Wasser-Zähler benachrichtigen?",
1505
+ "command": "getMeters",
1506
+ "jsonData": "{\"type\": \"water\"}",
1507
+ "multiple": true,
1508
+ "help": "Wähle die Zähler aus, für die du Benachrichtigungen erhalten möchtest. Wenn leer, werden ALLE Zähler benachrichtigt.",
1509
+ "hidden": "!data.notificationEnabled || !data.notificationWasserEnabled || !data.wasserAktiv",
1510
+ "sm": 12,
1511
+ "md": 8,
1512
+ "xs": 12,
1513
+ "lg": 8,
1514
+ "xl": 8
1515
+ },
1436
1516
  "notificationStromEnabled": {
1437
1517
  "type": "checkbox",
1438
1518
  "label": "Strom-Erinnerung",
@@ -1443,6 +1523,20 @@
1443
1523
  "lg": 4,
1444
1524
  "xl": 4
1445
1525
  },
1526
+ "notificationStromMeters": {
1527
+ "type": "selectSendTo",
1528
+ "label": "Welche Strom-Zähler benachrichtigen?",
1529
+ "command": "getMeters",
1530
+ "jsonData": "{\"type\": \"electricity\"}",
1531
+ "multiple": true,
1532
+ "help": "Wähle die Zähler aus, für die du Benachrichtigungen erhalten möchtest. Wenn leer, werden ALLE Zähler benachrichtigt.",
1533
+ "hidden": "!data.notificationEnabled || !data.notificationStromEnabled || !data.stromAktiv",
1534
+ "sm": 12,
1535
+ "md": 8,
1536
+ "xs": 12,
1537
+ "lg": 8,
1538
+ "xl": 8
1539
+ },
1446
1540
  "notificationPVEnabled": {
1447
1541
  "type": "checkbox",
1448
1542
  "label": "PV-Erinnerung",
@@ -1452,6 +1546,20 @@
1452
1546
  "xs": 12,
1453
1547
  "lg": 4,
1454
1548
  "xl": 4
1549
+ },
1550
+ "notificationPVMeters": {
1551
+ "type": "selectSendTo",
1552
+ "label": "Welche PV-Zähler benachrichtigen?",
1553
+ "command": "getMeters",
1554
+ "jsonData": "{\"type\": \"pv\"}",
1555
+ "multiple": true,
1556
+ "help": "Wähle die Zähler aus, für die du Benachrichtigungen erhalten möchtest. Wenn leer, werden ALLE Zähler benachrichtigt.",
1557
+ "hidden": "!data.notificationEnabled || !data.notificationPVEnabled || !data.pvAktiv",
1558
+ "sm": 12,
1559
+ "md": 8,
1560
+ "xs": 12,
1561
+ "lg": 8,
1562
+ "xl": 8
1455
1563
  }
1456
1564
  }
1457
1565
  },
@@ -1466,7 +1574,7 @@
1466
1574
  },
1467
1575
  "_infoVersion": {
1468
1576
  "type": "staticText",
1469
- "text": "**Version:** 1.4.0\n\n**Autor:** fischi87",
1577
+ "text": "**Version:** 1.4.6\n\n**Autor:** fischi87\n\n⚠️ **Breaking Change in v1.4.6:** State-Pfade haben sich geändert! Siehe unten.",
1470
1578
  "sm": 12,
1471
1579
  "xs": 12,
1472
1580
  "md": 12,
@@ -1492,7 +1600,7 @@
1492
1600
  },
1493
1601
  "_features": {
1494
1602
  "type": "staticText",
1495
- "text": "📊 **Verbrauchsüberwachung** - Gas, Wasser, Strom & PV/Einspeisung\n\n💰 **Automatische Kostenberechnung** - Mit Arbeitspreis & Grundgebühren\n\n☀️ **PV-Integration** - Überwachung von Einspeisung & Vergütung\n\n💳 **Abschlagsüberwachung** - Direkter Vergleich: Bezahlt vs. Verbraucht\n\n🔄 **Gas-Spezial** - Automatische m³ zu kWh Umrechnung\n\n⚡ **HT/NT-Unterstützung** - Volle Unterstützung für Doppeltarife\n\n📅 **Jahres-Historie** - Automatische Archivierung zum Vertragsbeginn",
1603
+ "text": "📊 **Verbrauchsüberwachung** - Gas, Wasser, Strom & PV/Einspeisung\n\n🔢 **Multi-Meter Support** *(NEU in 1.4.6)* - Mehrere Zähler pro Typ (z.B. Haus + Werkstatt)\n\n📛 **Konfigurierbare Namen** *(NEU in 1.4.6)* - Gib jedem Zähler einen individuellen Namen\n\n💰 **Automatische Kostenberechnung** - Mit Arbeitspreis & Grundgebühren\n\n☀️ **PV-Integration** - Überwachung von Einspeisung & Vergütung\n\n💳 **Abschlagsüberwachung** - Direkter Vergleich: Bezahlt vs. Verbraucht\n\n🔄 **Gas-Spezial** - Automatische m³ zu kWh Umrechnung\n\n⚡ **HT/NT-Unterstützung** - Volle Unterstützung für Doppeltarife\n\n📅 **Jahres-Historie** - Automatische Archivierung zum Vertragsbeginn\n\n🔔 **Smart Notifications** - Wähle für welche Zähler du benachrichtigt werden möchtest",
1496
1604
  "sm": 12,
1497
1605
  "xs": 12,
1498
1606
  "md": 12,
@@ -1502,14 +1610,31 @@
1502
1610
  "_divider2": {
1503
1611
  "type": "divider"
1504
1612
  },
1613
+ "_breakingHeader": {
1614
+ "type": "header",
1615
+ "text": "⚠️ Breaking Changes v1.4.6",
1616
+ "size": 4
1617
+ },
1618
+ "_breakingChanges": {
1619
+ "type": "staticText",
1620
+ "text": "**State-Pfade haben sich geändert:**\n\n**Vorher:** `gas.consumption.daily`, `gas.costs.yearly`\n\n**Neu:** `gas.METER_NAME.consumption.daily`, `gas.METER_NAME.costs.yearly`\n\n**Standardname:** Wenn du keinen Namen vergibst, wird automatisch \"main\" verwendet.\n\n**Was du tun musst:**\n• Skripte anpassen die States verwenden\n• Visualisierungen (VIS, Grafana) aktualisieren\n• History-Adapter neu konfigurieren\n\n**Mehrere Zähler:** Bei mehreren Zählern wird automatisch eine `totals/` Struktur mit Summen erstellt.",
1621
+ "sm": 12,
1622
+ "xs": 12,
1623
+ "md": 12,
1624
+ "lg": 12,
1625
+ "xl": 12
1626
+ },
1627
+ "_divider2a": {
1628
+ "type": "divider"
1629
+ },
1505
1630
  "_usageHeader": {
1506
1631
  "type": "header",
1507
- "text": "Benachrichtigungen (Neu)",
1632
+ "text": "📬 Benachrichtigungen",
1508
1633
  "size": 4
1509
1634
  },
1510
1635
  "_usage": {
1511
1636
  "type": "staticText",
1512
- "text": "**1. Abrechnungsende** 📅\n• Erinnert dich (z.B. 7 Tage vorher) daran, den Zählerstand abzulesen und den Zeitraum abzuschließen.\n\n**2. Vertragswechsel** 📋\n• Erinnert dich frühzeitig (z.B. 60 Tage vorher) daran, Tarife zu vergleichen oder die Kündigungsfrist zu prüfen.\n\n**3. Monatlicher Bericht** 📈\n• Optionaler Status-Bericht am 1. jedes Monats via Benachrichtigung.\n\n**💡 Tipp:** Nutze den **Test-Button** zum Prüfen deiner Einstellungen. Du bekommst sofort ein Popup mit dem Ergebnis!",
1637
+ "text": "**1. Abrechnungsende** 📅\n• Erinnert dich (z.B. 7 Tage vorher) daran, den Zählerstand abzulesen und den Zeitraum abzuschließen.\n\n**2. Vertragswechsel** 📋\n• Erinnert dich frühzeitig (z.B. 60 Tage vorher) daran, Tarife zu vergleichen oder die Kündigungsfrist zu prüfen.\n\n**3. Monatlicher Bericht** 📈\n• Optionaler Status-Bericht am 1. jedes Monats via Benachrichtigung.\n\n**4. Zählerauswahl** 🎯 *(NEU in 1.4.6)*\n• Wähle aus, für welche Zähler du Benachrichtigungen erhalten möchtest\n• Wenn leer: Alle Zähler werden benachrichtigt\n\n**💡 Tipp:** Nutze den **Test-Button** zum Prüfen deiner Einstellungen. Du bekommst sofort ein Popup mit dem Ergebnis!",
1513
1638
  "sm": 12,
1514
1639
  "xs": 12,
1515
1640
  "md": 12,
@@ -1526,7 +1651,7 @@
1526
1651
  },
1527
1652
  "_statesInfo": {
1528
1653
  "type": "staticText",
1529
- "text": "**📅 billing/** (Abrechnung)\n• **daysRemaining** - Tage bis zum Ende\n• **closePeriod** - Button zum Abschluss\n\n**💰 costs/** (Kosten)\n• **balance** - 🎯 Aktuelle Bilanz (Guthaben/Nachzahlung)\n• **totalYearly** - Prognose Gesamtkosten inkl. Gebühren\n\n**📈 statistics/** (Statistiken)\n• **lastDay** - Verbrauch gestern (kWh)\n• **lastDayVolume** - Verbrauch gestern in m³ (Gas)\n\n**☀️ PV / Einspeisung**\n• **earnings.totalYearly** - Akkumulierte Vergütung\n• **consumption.yearly** - Gesamte Einspeisung",
1654
+ "text": "**⚠️ Neue Struktur ab v1.4.6:**\n\nAlle Datenpunkte sind jetzt unter `TYPE.METER_NAME.*` zu finden!\n\n**Beispiel Single-Meter:**\n• `gas.main.consumption.daily`\n• `gas.main.costs.totalYearly`\n• `gas.main.billing.closePeriod`\n\n**Beispiel Multi-Meter:**\n• `gas.haus.consumption.yearly` ← Zähler 1\n• `gas.werkstatt.consumption.yearly` ← Zähler 2\n• `gas.totals.consumption.yearly` ← Summe\n\n**📅 billing/** (Abrechnung)\n• **daysRemaining** - Tage bis zum Vertragsende\n• **closePeriod** - Button zum Zeitraum-Abschluss\n• **endReading** - Hier Endzählerstand eintragen\n\n**💰 costs/** (Kosten)\n• **balance** - Aktuelle Bilanz (Nachzahlung/Guthaben)\n• **totalYearly** - Gesamtkosten inkl. Gebühren\n• **yearly** - Nur Verbrauchskosten\n\n**📈 statistics/** (Statistiken)\n• **lastDay** - Gestern-Verbrauch\n• **averageDaily** - Durchschnitt pro Tag\n• **averageMonthly** - Durchschnitt pro Monat",
1530
1655
  "sm": 12,
1531
1656
  "xs": 12,
1532
1657
  "md": 12,
package/io-package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "utility-monitor",
4
- "version": "1.4.5",
4
+ "version": "1.4.6",
5
5
  "news": {
6
+ "1.4.6": {
7
+ "en": "Breaking Change: Main meter now requires a name (default: 'main'). State paths changed from 'gas.*' to 'gas.METER_NAME.*'. All meters now use consistent naming structure.",
8
+ "de": "Breaking Change: Hauptzähler benötigt jetzt einen Namen (Standard: 'main'). State-Pfade geändert von 'gas.*' zu 'gas.METER_NAME.*'. Alle Zähler nutzen jetzt konsistente Namensstruktur."
9
+ },
6
10
  "1.4.5": {
7
11
  "en": "Fix: Critical multi-meter cost calculation bugs (main meter sync, basicCharge/paidTotal accumulation, annualFee as fixed yearly value). Fix: Balance formula corrected. Fix: Removed duplicate initialization causing sync issues.",
8
12
  "de": "Fix: Kritische Multi-Meter Kostenberechnungsfehler (Hauptzähler-Sync, basicCharge/paidTotal Akkumulation, Jahresgebühr als fester Jahreswert). Fix: Balance-Formel korrigiert. Fix: Doppelte Initialisierung entfernt.",
@@ -91,6 +95,7 @@
91
95
  },
92
96
  "native": {
93
97
  "gasAktiv": false,
98
+ "gasMainMeterName": "main",
94
99
  "gasSensorDP": "",
95
100
  "gasOffset": 0,
96
101
  "gasInitialReading": 0,
@@ -100,6 +105,7 @@
100
105
  "gasGrundgebuehr": 0,
101
106
  "gasAbschlag": 0,
102
107
  "wasserAktiv": false,
108
+ "wasserMainMeterName": "main",
103
109
  "wasserSensorDP": "",
104
110
  "wasserOffset": 0,
105
111
  "wasserInitialReading": 0,
@@ -107,6 +113,7 @@
107
113
  "wasserGrundgebuehr": 0,
108
114
  "wasserAbschlag": 0,
109
115
  "stromAktiv": false,
116
+ "stromMainMeterName": "main",
110
117
  "stromSensorDP": "",
111
118
  "stromOffset": 0,
112
119
  "stromInitialReading": 0,
@@ -114,6 +121,7 @@
114
121
  "stromGrundgebuehr": 0,
115
122
  "stromAbschlag": 0,
116
123
  "pvAktiv": false,
124
+ "pvMainMeterName": "main",
117
125
  "pvSensorDP": "",
118
126
  "pvOffset": 0,
119
127
  "pvInitialReading": 0,
@@ -125,8 +133,13 @@
125
133
  "notificationInstance": "",
126
134
  "notificationDaysBefore": 30,
127
135
  "notificationGasEnabled": false,
136
+ "notificationGasMeters": [],
128
137
  "notificationWasserEnabled": false,
138
+ "notificationWasserMeters": [],
129
139
  "notificationStromEnabled": false,
140
+ "notificationStromMeters": [],
141
+ "notificationPVEnabled": false,
142
+ "notificationPVMeters": [],
130
143
  "notificationBillingEnabled": true,
131
144
  "notificationBillingDays": 7,
132
145
  "notificationChangeDays": 60
@@ -422,7 +422,7 @@ class BillingManager {
422
422
  * @param {object} meter - Meter object from multiMeterManager
423
423
  */
424
424
  async closeBillingPeriodForMeter(type, meter) {
425
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
425
+ const basePath = `${type}.${meter.name}`;
426
426
  const label = meter.displayName || meter.name;
427
427
 
428
428
  this.adapter.log.info(`🔔 Schließe Abrechnungszeitraum für ${basePath} (${label})...`);
@@ -438,14 +438,8 @@ class BillingManager {
438
438
  return;
439
439
  }
440
440
 
441
- // Get contract date for THIS meter
442
- let contractStartDate;
443
- if (meter.name === 'main') {
444
- const configType = this.adapter.consumptionManager.getConfigType(type);
445
- contractStartDate = this.adapter.config[`${configType}ContractStart`];
446
- } else {
447
- contractStartDate = meter.config?.contractStart;
448
- }
441
+ // Get contract date for THIS meter (all meters have config.contractStart)
442
+ const contractStartDate = meter.config?.contractStart;
449
443
 
450
444
  if (!contractStartDate) {
451
445
  this.adapter.log.error(`❌ Kein Vertragsbeginn für ${basePath} konfiguriert.`);
@@ -497,42 +491,49 @@ class BillingManager {
497
491
  }
498
492
 
499
493
  /**
500
- * Updates billing countdown
494
+ * Updates billing countdown for all meters of a type
495
+ * NOTE: Since v1.4.6, this updates ALL meters (main + additional)
501
496
  *
502
497
  * @param {string} type - Utility type
503
498
  */
504
499
  async updateBillingCountdown(type) {
505
- const configType = this.adapter.consumptionManager.getConfigType(type);
506
- const contractStart = this.adapter.config[`${configType}ContractStart`];
500
+ // Get all meters for this type
501
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
507
502
 
508
- if (!contractStart) {
509
- return;
510
- }
503
+ // Update countdown for each meter based on its contract date
504
+ for (const meter of meters) {
505
+ const contractStart = meter.config?.contractStart;
511
506
 
512
- const startDate = calculator.parseGermanDate(contractStart);
513
- if (!startDate) {
514
- return;
515
- }
507
+ if (!contractStart) {
508
+ continue;
509
+ }
516
510
 
517
- const today = new Date();
518
- const nextAnniversary = new Date(startDate);
519
- nextAnniversary.setFullYear(today.getFullYear());
511
+ const startDate = calculator.parseGermanDate(contractStart);
512
+ if (!startDate) {
513
+ continue;
514
+ }
520
515
 
521
- if (nextAnniversary < today) {
522
- nextAnniversary.setFullYear(today.getFullYear() + 1);
523
- }
516
+ const today = new Date();
517
+ const nextAnniversary = new Date(startDate);
518
+ nextAnniversary.setFullYear(today.getFullYear());
524
519
 
525
- const msPerDay = 1000 * 60 * 60 * 24;
526
- const daysRemaining = Math.ceil((nextAnniversary.getTime() - today.getTime()) / msPerDay);
527
- const displayPeriodEnd = new Date(nextAnniversary);
528
- displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
520
+ if (nextAnniversary < today) {
521
+ nextAnniversary.setFullYear(today.getFullYear() + 1);
522
+ }
529
523
 
530
- await this.adapter.setStateAsync(`${type}.billing.daysRemaining`, daysRemaining, true);
531
- await this.adapter.setStateAsync(
532
- `${type}.billing.periodEnd`,
533
- displayPeriodEnd.toLocaleDateString('de-DE'),
534
- true,
535
- );
524
+ const msPerDay = 1000 * 60 * 60 * 24;
525
+ const daysRemaining = Math.ceil((nextAnniversary.getTime() - today.getTime()) / msPerDay);
526
+ const displayPeriodEnd = new Date(nextAnniversary);
527
+ displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
528
+
529
+ const basePath = `${type}.${meter.name}`;
530
+ await this.adapter.setStateAsync(`${basePath}.billing.daysRemaining`, daysRemaining, true);
531
+ await this.adapter.setStateAsync(
532
+ `${basePath}.billing.periodEnd`,
533
+ displayPeriodEnd.toLocaleDateString('de-DE'),
534
+ true,
535
+ );
536
+ }
536
537
  }
537
538
 
538
539
  /**
@@ -580,21 +581,14 @@ class BillingManager {
580
581
  // YEARLY RESET: Each meter resets individually based on ITS contract date
581
582
  const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
582
583
  for (const meter of meters) {
583
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
584
+ const basePath = `${type}.${meter.name}`;
584
585
  const lastYearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
585
586
 
586
587
  if (lastYearStartState?.val) {
587
588
  const lastYearStartDate = new Date(lastYearStartState.val);
588
589
 
589
- // Get contract date for THIS specific meter
590
- let contractStartDate;
591
- if (meter.name === 'main') {
592
- // Main meter: use adapter config
593
- contractStartDate = this.adapter.config[`${configType}ContractStart`];
594
- } else {
595
- // Additional meter: use meter's individual config
596
- contractStartDate = meter.config?.contractStart;
597
- }
590
+ // Get contract date for THIS specific meter (all meters have config.contractStart)
591
+ const contractStartDate = meter.config?.contractStart;
598
592
 
599
593
  if (contractStartDate) {
600
594
  const contractStart = calculator.parseGermanDate(contractStartDate);
@@ -650,7 +644,7 @@ class BillingManager {
650
644
 
651
645
  // Reset each meter
652
646
  for (const meter of meters) {
653
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
647
+ const basePath = `${type}.${meter.name}`;
654
648
  const label = meter.displayName || meter.name;
655
649
 
656
650
  this.adapter.log.debug(`Resetting daily counter for ${basePath} (${label})`);
@@ -719,7 +713,7 @@ class BillingManager {
719
713
 
720
714
  // Reset each meter
721
715
  for (const meter of meters) {
722
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
716
+ const basePath = `${type}.${meter.name}`;
723
717
  const label = meter.displayName || meter.name;
724
718
 
725
719
  this.adapter.log.debug(`Resetting monthly counter for ${basePath} (${label})`);
@@ -777,7 +771,7 @@ class BillingManager {
777
771
 
778
772
  // Reset each meter
779
773
  for (const meter of meters) {
780
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
774
+ const basePath = `${type}.${meter.name}`;
781
775
  const label = meter.displayName || meter.name;
782
776
 
783
777
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
@@ -809,7 +803,7 @@ class BillingManager {
809
803
  * @param {object} meter - Meter object from multiMeterManager
810
804
  */
811
805
  async resetYearlyCountersForMeter(type, meter) {
812
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
806
+ const basePath = `${type}.${meter.name}`;
813
807
  const label = meter.displayName || meter.name;
814
808
 
815
809
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
@@ -47,8 +47,8 @@ class ConsumptionManager {
47
47
 
48
48
  this.adapter.log.info(`Initializing ${type} monitoring...`);
49
49
 
50
- // Create state structure
51
- await stateManager.createUtilityStateStructure(this.adapter, type, this.adapter.config);
50
+ // State structure is now created by MultiMeterManager per meter (v1.4.6)
51
+ // Old createUtilityStateStructure removed - states are created under type.meterName.*
52
52
 
53
53
  const configType = this.getConfigType(type);
54
54
  const sensorDPKey = `${configType}SensorDP`;
@@ -56,7 +56,7 @@ class ConsumptionManager {
56
56
 
57
57
  if (!sensorDP) {
58
58
  this.adapter.log.warn(`${type} is active but no sensor datapoint configured!`);
59
- await this.adapter.setStateAsync(`${type}.info.sensorActive`, false, true);
59
+ // Note: sensorActive state is now created per meter by MultiMeterManager
60
60
  return;
61
61
  }
62
62
 
@@ -69,91 +69,34 @@ class ConsumptionManager {
69
69
  this.adapter.log.info(`${type}: Managed with contract start: ${contractStartDateStr}`);
70
70
  }
71
71
 
72
- // Subscribe to sensor datapoint
73
- this.adapter.subscribeForeignStates(sensorDP);
74
- await this.adapter.setStateAsync(`${type}.info.sensorActive`, true, true);
75
- this.adapter.log.debug(`Subscribed to ${type} sensor: ${sensorDP}`);
72
+ // Sensor subscription is now handled by MultiMeterManager per meter
73
+ this.adapter.log.debug(`${type} sensor will be subscribed by MultiMeterManager: ${sensorDP}`);
76
74
 
77
75
  // Initialize all meters (main + additional) via MultiMeterManager
76
+ // This handles everything now: state creation, sensor subscription, costs calculation
78
77
  if (this.adapter.multiMeterManager) {
79
78
  await this.adapter.multiMeterManager.initializeType(type);
80
79
  }
81
80
 
82
- // Restore last sensor value from persistent state to prevent delta loss
83
- const lastReading = await this.adapter.getStateAsync(`${type}.info.meterReading`);
84
- if (lastReading && typeof lastReading.val === 'number') {
85
- this.lastSensorValues[sensorDP] = lastReading.val;
86
- this.adapter.log.debug(`${type}: Restored last sensor value: ${lastReading.val}`);
87
- }
88
-
89
- // Initialize with current sensor value
90
- try {
91
- const sensorState = await this.adapter.getForeignStateAsync(sensorDP);
92
- if (sensorState && sensorState.val !== null && typeof sensorState.val === 'number') {
93
- await this.handleSensorUpdate(type, sensorDP, sensorState.val);
94
- }
95
- } catch (error) {
96
- this.adapter.log.warn(`Could not read initial value from ${sensorDP}: ${error.message}`);
97
- }
98
-
99
- // Initialize period start timestamps if not set
100
- const now = Date.now();
101
- const dayStart = await this.adapter.getStateAsync(`${type}.statistics.lastDayStart`);
102
- if (!dayStart || !dayStart.val) {
103
- await this.adapter.setStateAsync(`${type}.statistics.lastDayStart`, now, true);
104
- }
105
-
106
- const monthStart = await this.adapter.getStateAsync(`${type}.statistics.lastMonthStart`);
107
- if (!monthStart || !monthStart.val) {
108
- await this.adapter.setStateAsync(`${type}.statistics.lastMonthStart`, now, true);
109
- }
110
-
111
- const yearStart = await this.adapter.getStateAsync(`${type}.statistics.lastYearStart`);
112
- if (!yearStart || !yearStart.val) {
113
- // Determine year start based on contract date or January 1st
114
- const contractStartKey = `${configType}ContractStart`;
115
- const contractStartDateStr = this.adapter.config[contractStartKey];
116
-
117
- let yearStartDate;
118
- if (contractStartDateStr) {
119
- const contractStart = calculator.parseGermanDate(contractStartDateStr);
120
- if (contractStart && !isNaN(contractStart.getTime())) {
121
- // Calculate last anniversary
122
- const nowDate = new Date(now);
123
- const currentYear = nowDate.getFullYear();
124
- yearStartDate = new Date(currentYear, contractStart.getMonth(), contractStart.getDate(), 12, 0, 0);
125
-
126
- // If anniversary is in the future this year, take last year
127
- if (yearStartDate > nowDate) {
128
- yearStartDate.setFullYear(currentYear - 1);
129
- }
130
- }
131
- }
132
-
133
- if (!yearStartDate) {
134
- // Fallback: January 1st of current year
135
- const nowDate = new Date(now);
136
- yearStartDate = new Date(nowDate.getFullYear(), 0, 1, 12, 0, 0);
137
- this.adapter.log.info(
138
- `${type}: No contract start found. Setting initial year start to January 1st: ${yearStartDate.toLocaleDateString('de-DE')}`,
139
- );
140
- }
141
-
142
- await this.adapter.setStateAsync(`${type}.statistics.lastYearStart`, yearStartDate.getTime(), true);
143
- }
144
- // Update current price
145
- await this.updateCurrentPrice(type);
146
-
147
- // Initial cost calculation (wichtig! Sonst bleiben Kosten bei 0)
148
- if (typeof this.adapter.updateCosts === 'function') {
149
- await this.adapter.updateCosts(type);
150
- }
81
+ // Note: All initialization moved to MultiMeterManager in v1.4.6:
82
+ // - Sensor value restoration (per meter)
83
+ // - Period start timestamps (per meter)
84
+ // - Current price updates (per meter)
85
+ // - Cost calculations (per meter)
86
+ // Old type-level states (gas.info.*, gas.statistics.*) are no longer used
151
87
 
152
88
  // Initialize yearly consumption from initial reading if set
89
+ // NOTE: This is now handled per meter by MultiMeterManager in v1.4.6
90
+ // This legacy code path should not execute for new setups, but is kept for safety
153
91
  const initialReadingKey = `${configType}InitialReading`;
154
92
  const initialReading = this.adapter.config[initialReadingKey] || 0;
155
93
 
156
- if (initialReading > 0) {
94
+ if (initialReading > 0 && sensorDP) {
95
+ // Get the main meter name to use the correct path
96
+ const mainMeterNameKey = `${configType}MainMeterName`;
97
+ const mainMeterName = this.adapter.config[mainMeterNameKey] || 'main';
98
+ const basePath = `${type}.${mainMeterName}`;
99
+
157
100
  const sensorState = await this.adapter.getForeignStateAsync(sensorDP);
158
101
  if (sensorState && typeof sensorState.val === 'number') {
159
102
  let currentRaw = sensorState.val;
@@ -173,7 +116,7 @@ class ConsumptionManager {
173
116
  const zZahl = this.adapter.config.gasZahl || 0.95;
174
117
  const yearlyVolume = yearlyConsumption;
175
118
  yearlyConsumption = calculator.convertGasM3ToKWh(yearlyConsumption, brennwert, zZahl);
176
- await this.adapter.setStateAsync(`${type}.consumption.yearlyVolume`, yearlyVolume, true);
119
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, yearlyVolume, true);
177
120
  this.adapter.log.info(
178
121
  `Init yearly ${type}: ${yearlyConsumption.toFixed(2)} kWh = ${(currentRaw - initialReading).toFixed(2)} m³ (current: ${currentRaw.toFixed(2)} m³, initial: ${initialReading} m³)`,
179
122
  );
@@ -183,17 +126,15 @@ class ConsumptionManager {
183
126
  );
184
127
  }
185
128
 
186
- await this.adapter.setStateAsync(`${type}.consumption.yearly`, yearlyConsumption, true);
129
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyConsumption, true);
187
130
  if (typeof this.adapter.updateCosts === 'function') {
188
131
  await this.adapter.updateCosts(type);
189
132
  }
190
133
  }
191
134
  }
192
135
 
193
- // Update billing countdown
194
- if (typeof this.adapter.updateBillingCountdown === 'function') {
195
- await this.adapter.updateBillingCountdown(type);
196
- }
136
+ // Note: Billing countdown is now handled per meter by billingManager.updateBillingCountdown()
137
+ // which is called during checkPeriodResets()
197
138
 
198
139
  this.adapter.log.debug(`Initial cost calculation completed for ${type}`);
199
140
  }
@@ -201,17 +142,25 @@ class ConsumptionManager {
201
142
  /**
202
143
  * Handles sensor value updates
203
144
  *
145
+ * NOTE: This method is DEPRECATED since v1.4.6!
146
+ * All sensor updates are now handled by MultiMeterManager.handleSensorUpdate()
147
+ * This method remains only as fallback but should NEVER be called in normal operation.
148
+ *
204
149
  * @param {string} type - Utility type
205
150
  * @param {string} sensorDP - Sensor datapoint ID
206
151
  * @param {number} value - New sensor value
207
152
  */
208
153
  async handleSensorUpdate(type, sensorDP, value) {
154
+ this.adapter.log.warn(
155
+ `consumptionManager.handleSensorUpdate() called - this is deprecated! All sensors should be handled by MultiMeterManager.`,
156
+ );
157
+
209
158
  if (typeof value !== 'number' || value < 0) {
210
159
  this.adapter.log.warn(`Invalid sensor value for ${type}: ${value}`);
211
160
  return;
212
161
  }
213
162
 
214
- this.adapter.log.debug(`Sensor update for ${type}: ${value}`);
163
+ this.adapter.log.debug(`[DEPRECATED] Sensor update for ${type}: ${value}`);
215
164
 
216
165
  const now = Date.now();
217
166
  let consumption = value;
@@ -371,36 +320,47 @@ class ConsumptionManager {
371
320
  }
372
321
 
373
322
  /**
374
- * Updates the current price display
323
+ * Updates the current price display for all meters of a type
324
+ * NOTE: Since v1.4.6, this updates ALL meters (main + additional)
325
+ * Only main meters support HT/NT, additional meters have fixed price
375
326
  *
376
327
  * @param {string} type - Utility type
377
328
  */
378
329
  async updateCurrentPrice(type) {
379
330
  const configType = this.getConfigType(type);
380
331
 
381
- // Check for HT/NT
382
- const htNtEnabledKey = `${configType}HtNtEnabled`;
383
- const htNtEnabled = this.adapter.config[htNtEnabledKey] || false;
332
+ // Get all meters for this type
333
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
384
334
 
385
- let tariffName = 'Standard';
386
- let activePrice = 0;
335
+ for (const meter of meters) {
336
+ let tariffName = 'Standard';
337
+ let activePrice = 0;
387
338
 
388
- if (htNtEnabled) {
389
- const isHT = calculator.isHTTime(this.adapter.config, configType);
390
- if (isHT) {
391
- activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
392
- tariffName = 'Haupttarif (HT)';
339
+ // Check if this meter has HT/NT enabled
340
+ const htNtEnabled = meter.config?.htNtEnabled || false;
341
+
342
+ if (htNtEnabled) {
343
+ const isHT = calculator.isHTTime(this.adapter.config, configType);
344
+ if (isHT) {
345
+ activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
346
+ tariffName = 'Haupttarif (HT)';
347
+ } else {
348
+ activePrice = this.adapter.config[`${configType}NtPrice`] || 0;
349
+ tariffName = 'Nebentarif (NT)';
350
+ }
393
351
  } else {
394
- activePrice = this.adapter.config[`${configType}NtPrice`] || 0;
395
- tariffName = 'Nebentarif (NT)';
352
+ // Use meter's configured price
353
+ activePrice = meter.config?.preis || 0;
396
354
  }
397
- } else {
398
- const priceKey = `${configType}Preis`;
399
- activePrice = this.adapter.config[priceKey] || 0;
400
- }
401
355
 
402
- await this.adapter.setStateAsync(`${type}.info.currentPrice`, calculator.roundToDecimals(activePrice, 4), true);
403
- await this.adapter.setStateAsync(`${type}.info.currentTariff`, tariffName, true);
356
+ const basePath = `${type}.${meter.name}`;
357
+ await this.adapter.setStateAsync(
358
+ `${basePath}.info.currentPrice`,
359
+ calculator.roundToDecimals(activePrice, 4),
360
+ true,
361
+ );
362
+ await this.adapter.setStateAsync(`${basePath}.info.currentTariff`, tariffName, true);
363
+ }
404
364
  }
405
365
  }
406
366
 
@@ -24,7 +24,36 @@ class MessagingHandler {
24
24
 
25
25
  this.adapter.log.debug(`[onMessage] Received command: ${obj.command} from ${obj.from}`);
26
26
 
27
- if (obj.command === 'getInstances') {
27
+ if (obj.command === 'getMeters') {
28
+ try {
29
+ // Get the utility type from the message
30
+ const type = obj.message?.type;
31
+
32
+ if (!type) {
33
+ this.adapter.sendTo(obj.from, obj.command, [], obj.callback);
34
+ return;
35
+ }
36
+
37
+ // Get all meters for this type from multiMeterManager
38
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
39
+
40
+ if (meters.length === 0) {
41
+ this.adapter.sendTo(obj.from, obj.command, [], obj.callback);
42
+ return;
43
+ }
44
+
45
+ // Build options array: [{ value: "main", label: "Hauptzähler (main)" }, ...]
46
+ const result = meters.map(meter => ({
47
+ value: meter.name,
48
+ label: meter.displayName ? `${meter.displayName} (${meter.name})` : meter.name,
49
+ }));
50
+
51
+ this.adapter.sendTo(obj.from, obj.command, result, obj.callback);
52
+ } catch (error) {
53
+ this.adapter.log.error(`Error in getMeters callback: ${error.message}`);
54
+ this.adapter.sendTo(obj.from, obj.command, [], obj.callback);
55
+ }
56
+ } else if (obj.command === 'getInstances') {
28
57
  try {
29
58
  const instances = await this.adapter.getForeignObjectsAsync('system.adapter.*', 'instance');
30
59
  const messengerTypes = [
@@ -163,43 +192,66 @@ class MessagingHandler {
163
192
  continue;
164
193
  }
165
194
 
166
- // Get current days remaining
167
- const daysRemainingState = await this.adapter.getStateAsync(`${type}.billing.daysRemaining`);
168
- const daysRemaining = typeof daysRemainingState?.val === 'number' ? daysRemainingState.val : 999;
169
- const periodEndState = await this.adapter.getStateAsync(`${type}.billing.periodEnd`);
170
- const periodEnd = periodEndState?.val || '--.--.----';
171
-
172
- // 1. BILLING END REMINDER (Zählerstand ablesen)
173
- if (this.adapter.config.notificationBillingEnabled) {
174
- const billingSent = await this.adapter.getStateAsync(`${type}.billing.notificationSent`);
175
- const billingDaysThreshold = this.adapter.config.notificationBillingDays || 7;
176
-
177
- if (billingSent?.val !== true && daysRemaining <= billingDaysThreshold) {
178
- const message =
179
- `🔔 *Nebenkosten-Monitor: Zählerstand ablesen*\n\n` +
180
- `Dein Abrechnungszeitraum für *${typesDe[type]}* endet in ${daysRemaining} Tagen!\n\n` +
181
- `📅 Datum: ${periodEnd}\n\n` +
182
- `Bitte trage den Zählerstand rechtzeitig ein:\n` +
183
- `1️⃣ Datenpunkt: ${type}.billing.endReading\n` +
184
- `2️⃣ Zeitraum abschließen: ${type}.billing.closePeriod = true`;
185
-
186
- await this.sendNotification(type, message, 'billing');
187
- }
195
+ // Get all meters for this type (main + additional)
196
+ const allMeters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
197
+ if (allMeters.length === 0) {
198
+ continue;
199
+ }
200
+
201
+ // Filter meters based on configuration
202
+ // If notificationXXXMeters is empty or undefined, notify ALL meters
203
+ const configKey = `notification${configType.charAt(0).toUpperCase() + configType.slice(1)}Meters`;
204
+ const selectedMeters = this.adapter.config[configKey];
205
+
206
+ let metersToNotify = allMeters;
207
+ if (selectedMeters && Array.isArray(selectedMeters) && selectedMeters.length > 0) {
208
+ // Filter: only notify selected meters
209
+ metersToNotify = allMeters.filter(meter => selectedMeters.includes(meter.name));
188
210
  }
189
211
 
190
- // 2. CONTRACT CHANGE REMINDER (Tarif wechseln / Kündigungsfrist)
191
- if (this.adapter.config.notificationChangeEnabled) {
192
- const changeSent = await this.adapter.getStateAsync(`${type}.billing.notificationChangeSent`);
193
- const changeDaysThreshold = this.adapter.config.notificationChangeDays || 60;
212
+ // Check notifications for each meter individually
213
+ for (const meter of metersToNotify) {
214
+ const basePath = `${type}.${meter.name}`;
215
+ const meterLabel = meter.displayName || meter.name;
216
+
217
+ // Get current days remaining for THIS meter
218
+ const daysRemainingState = await this.adapter.getStateAsync(`${basePath}.billing.daysRemaining`);
219
+ const daysRemaining = typeof daysRemainingState?.val === 'number' ? daysRemainingState.val : 999;
220
+ const periodEndState = await this.adapter.getStateAsync(`${basePath}.billing.periodEnd`);
221
+ const periodEnd = periodEndState?.val || '--.--.----';
222
+
223
+ // 1. BILLING END REMINDER (Zählerstand ablesen)
224
+ if (this.adapter.config.notificationBillingEnabled) {
225
+ const billingSent = await this.adapter.getStateAsync(`${basePath}.billing.notificationSent`);
226
+ const billingDaysThreshold = this.adapter.config.notificationBillingDays || 7;
227
+
228
+ if (billingSent?.val !== true && daysRemaining <= billingDaysThreshold) {
229
+ const message =
230
+ `🔔 *Nebenkosten-Monitor: Zählerstand ablesen*\n\n` +
231
+ `Dein Abrechnungszeitraum für *${typesDe[type]} (${meterLabel})* endet in ${daysRemaining} Tagen!\n\n` +
232
+ `📅 Datum: ${periodEnd}\n\n` +
233
+ `Bitte trage den Zählerstand rechtzeitig ein:\n` +
234
+ `1️⃣ Datenpunkt: ${basePath}.billing.endReading\n` +
235
+ `2️⃣ Zeitraum abschließen: ${basePath}.billing.closePeriod = true`;
236
+
237
+ await this.sendNotification(basePath, message, 'billing');
238
+ }
239
+ }
240
+
241
+ // 2. CONTRACT CHANGE REMINDER (Tarif wechseln / Kündigungsfrist)
242
+ if (this.adapter.config.notificationChangeEnabled) {
243
+ const changeSent = await this.adapter.getStateAsync(`${basePath}.billing.notificationChangeSent`);
244
+ const changeDaysThreshold = this.adapter.config.notificationChangeDays || 60;
194
245
 
195
- if (changeSent?.val !== true && daysRemaining <= changeDaysThreshold) {
196
- const message =
197
- `💡 *Nebenkosten-Monitor: Tarif-Check*\n\n` +
198
- `Dein Vertrag für *${typesDe[type]}* endet am ${periodEnd}.\n\n` +
199
- `⏰ Noch ${daysRemaining} Tage bis zum Ende des Zeitraums.\n\n` +
200
- `Jetzt ist ein guter Zeitpunkt, um Preise zu vergleichen oder die Kündigungsfrist zu prüfen! 💸`;
246
+ if (changeSent?.val !== true && daysRemaining <= changeDaysThreshold) {
247
+ const message =
248
+ `💡 *Nebenkosten-Monitor: Tarif-Check*\n\n` +
249
+ `Dein Vertrag für *${typesDe[type]} (${meterLabel})* endet am ${periodEnd}.\n\n` +
250
+ `⏰ Noch ${daysRemaining} Tage bis zum Ende des Zeitraums.\n\n` +
251
+ `Jetzt ist ein guter Zeitpunkt, um Preise zu vergleichen oder die Kündigungsfrist zu prüfen! 💸`;
201
252
 
202
- await this.sendNotification(type, message, 'change');
253
+ await this.sendNotification(basePath, message, 'change');
254
+ }
203
255
  }
204
256
  }
205
257
  }
@@ -258,16 +310,24 @@ class MessagingHandler {
258
310
  // Multi-meter: use totals
259
311
  yearlyState = await this.adapter.getStateAsync(`${type}.totals.consumption.yearly`);
260
312
  totalYearlyState = await this.adapter.getStateAsync(`${type}.totals.costs.totalYearly`);
261
- // Balance/paidTotal not available in totals, use main meter as representative
262
- paidTotalState = await this.adapter.getStateAsync(`${type}.costs.paidTotal`);
263
- balanceState = await this.adapter.getStateAsync(`${type}.costs.balance`);
313
+ // Balance/paidTotal not available in totals, use first meter as representative
314
+ const firstMeter = meters[0];
315
+ if (firstMeter) {
316
+ paidTotalState = await this.adapter.getStateAsync(`${type}.${firstMeter.name}.costs.paidTotal`);
317
+ balanceState = await this.adapter.getStateAsync(`${type}.${firstMeter.name}.costs.balance`);
318
+ }
264
319
  message += `(${meters.length} Zähler gesamt)\\n`;
320
+ } else if (meters.length === 1) {
321
+ // Single meter: use first meter values (new path structure)
322
+ const meter = meters[0];
323
+ const basePath = `${type}.${meter.name}`;
324
+ yearlyState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
325
+ totalYearlyState = await this.adapter.getStateAsync(`${basePath}.costs.totalYearly`);
326
+ paidTotalState = await this.adapter.getStateAsync(`${basePath}.costs.paidTotal`);
327
+ balanceState = await this.adapter.getStateAsync(`${basePath}.costs.balance`);
265
328
  } else {
266
- // Single meter: use main meter values
267
- yearlyState = await this.adapter.getStateAsync(`${type}.consumption.yearly`);
268
- totalYearlyState = await this.adapter.getStateAsync(`${type}.costs.totalYearly`);
269
- paidTotalState = await this.adapter.getStateAsync(`${type}.costs.paidTotal`);
270
- balanceState = await this.adapter.getStateAsync(`${type}.costs.balance`);
329
+ // No meters configured - skip this type
330
+ continue;
271
331
  }
272
332
 
273
333
  let val = yearlyState?.val || 0;
@@ -310,14 +370,14 @@ class MessagingHandler {
310
370
  /**
311
371
  * Helper to send notification and mark as sent
312
372
  *
313
- * @param {string} type - gas, water, electricity
373
+ * @param {string} pathOrType - Full path like "gas.main" or just "system" for reports
314
374
  * @param {string} message - Message text
315
375
  * @param {string} reminderType - billing, change, or report
316
376
  */
317
- async sendNotification(type, message, reminderType) {
377
+ async sendNotification(pathOrType, message, reminderType) {
318
378
  try {
319
379
  const instance = this.adapter.config.notificationInstance;
320
- this.adapter.log.info(`Sending ${reminderType} notification for ${type} via ${instance}`);
380
+ this.adapter.log.info(`Sending ${reminderType} notification for ${pathOrType} via ${instance}`);
321
381
 
322
382
  await this.adapter.sendToAsync(instance, 'send', {
323
383
  text: message,
@@ -326,12 +386,13 @@ class MessagingHandler {
326
386
  });
327
387
 
328
388
  // Mark as sent (only for billing/change)
389
+ // pathOrType is now the full path like "gas.main" or "gas.werkstatt"
329
390
  if (reminderType !== 'report') {
330
391
  const stateKey = reminderType === 'change' ? 'notificationChangeSent' : 'notificationSent';
331
- await this.adapter.setStateAsync(`${type}.billing.${stateKey}`, true, true);
392
+ await this.adapter.setStateAsync(`${pathOrType}.billing.${stateKey}`, true, true);
332
393
  }
333
394
  } catch (error) {
334
- this.adapter.log.error(`Failed to send ${reminderType} notification for ${type}: ${error.message}`);
395
+ this.adapter.log.error(`Failed to send ${reminderType} notification for ${pathOrType}: ${error.message}`);
335
396
  }
336
397
  }
337
398
  }
@@ -70,8 +70,14 @@ class MultiMeterManager {
70
70
  // Main meter (always present if type is active)
71
71
  const mainActive = this.adapter.config[`${configType}Aktiv`];
72
72
  if (mainActive) {
73
+ // Get main meter name from config and normalize
74
+ const mainMeterName = this.adapter.config[`${configType}MainMeterName`] || 'main';
75
+ const normalizedName = this.normalizeMeterName(mainMeterName);
76
+ const displayName = mainMeterName; // Original name for display
77
+
73
78
  meters.push({
74
- name: 'main',
79
+ name: normalizedName,
80
+ displayName: displayName,
75
81
  config: {
76
82
  sensorDP: this.adapter.config[`${configType}SensorDP`],
77
83
  preis: parseConfigNumber(this.adapter.config[`${configType}Preis`], 0),
@@ -173,7 +179,7 @@ class MultiMeterManager {
173
179
  * @returns {Promise<void>}
174
180
  */
175
181
  async initializeMeter(type, meterName, config, displayName) {
176
- const basePath = meterName === 'main' ? type : `${type}.${meterName}`;
182
+ const basePath = `${type}.${meterName}`;
177
183
  const label = displayName || meterName;
178
184
 
179
185
  this.adapter.log.info(`Initializing ${type} meter: ${label}`);
@@ -349,7 +355,7 @@ class MultiMeterManager {
349
355
  return;
350
356
  }
351
357
 
352
- const basePath = meterName === 'main' ? type : `${type}.${meterName}`;
358
+ const basePath = `${type}.${meterName}`;
353
359
  this.adapter.log.debug(`Sensor update for ${basePath}: ${value}`);
354
360
 
355
361
  // Get meter config
@@ -491,14 +497,14 @@ class MultiMeterManager {
491
497
  * @param {object} config - Meter configuration
492
498
  */
493
499
  async updateCurrentPrice(type, meterName, config) {
494
- const basePath = meterName === 'main' ? type : `${type}.${meterName}`;
500
+ const basePath = `${type}.${meterName}`;
495
501
  const configType = this.getConfigType(type);
496
502
 
497
503
  let tariffName = 'Standard';
498
504
  let activePrice = config.preis || 0;
499
505
 
500
- // Only main meter supports HT/NT
501
- if (meterName === 'main' && config.htNtEnabled) {
506
+ // Only meters with HT/NT enabled support it
507
+ if (config.htNtEnabled) {
502
508
  const isHT = calculator.isHTTime(this.adapter.config, configType);
503
509
  if (isHT) {
504
510
  activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
@@ -526,7 +532,7 @@ class MultiMeterManager {
526
532
  * @param {object} config - Meter configuration
527
533
  */
528
534
  async updateCosts(type, meterName, config) {
529
- const basePath = meterName === 'main' ? type : `${type}.${meterName}`;
535
+ const basePath = `${type}.${meterName}`;
530
536
 
531
537
  // Get consumption values
532
538
  const daily = (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
@@ -626,7 +632,7 @@ class MultiMeterManager {
626
632
  let totalCostsYearly = 0;
627
633
 
628
634
  for (const meter of meters) {
629
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
635
+ const basePath = `${type}.${meter.name}`;
630
636
 
631
637
  totalDaily += (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
632
638
  totalMonthly += (await this.adapter.getStateAsync(`${basePath}.consumption.monthly`))?.val || 0;
@@ -817,11 +817,12 @@ async function createUtilityStateStructure(adapter, type, _config = {}) {
817
817
  async function createMeterStructure(adapter, type, meterName, _config = {}) {
818
818
  const isGas = type === 'gas';
819
819
 
820
+ // All meters now include the meter name in labels for consistency
820
821
  const labels = {
821
- gas: { name: meterName === 'main' ? 'Gas' : `Gas (${meterName})`, unit: 'kWh', volumeUnit: 'm³' },
822
- water: { name: meterName === 'main' ? 'Wasser' : `Wasser (${meterName})`, unit: 'm³', volumeUnit: 'm³' },
823
- electricity: { name: meterName === 'main' ? 'Strom' : `Strom (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
824
- pv: { name: meterName === 'main' ? 'PV' : `PV (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
822
+ gas: { name: `Gas (${meterName})`, unit: 'kWh', volumeUnit: 'm³' },
823
+ water: { name: `Wasser (${meterName})`, unit: 'm³', volumeUnit: 'm³' },
824
+ electricity: { name: `Strom (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
825
+ pv: { name: `PV (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
825
826
  };
826
827
 
827
828
  const label = labels[type];
@@ -832,7 +833,7 @@ async function createMeterStructure(adapter, type, meterName, _config = {}) {
832
833
  // Fallback to prevent crash
833
834
  return;
834
835
  }
835
- const basePath = meterName === 'main' ? type : `${type}.${meterName}`;
836
+ const basePath = `${type}.${meterName}`;
836
837
 
837
838
  // Create main channel
838
839
  await adapter.setObjectNotExistsAsync(basePath, {
package/main.js CHANGED
@@ -195,20 +195,15 @@ class UtilityMonitor extends utils.Adapter {
195
195
  if (id.includes('.billing.closePeriod') && state.val === true && !state.ack) {
196
196
  const parts = id.split('.');
197
197
 
198
- // Parse state ID: nebenkosten-monitor.0.gas.erdgeschoss.billing.closePeriod
199
- // Remove adapter prefix: gas.erdgeschoss.billing.closePeriod
198
+ // Parse state ID: nebenkosten-monitor.0.gas.meterName.billing.closePeriod
199
+ // Remove adapter prefix: gas.meterName.billing.closePeriod
200
200
  const statePathParts = parts.slice(2); // Remove "nebenkosten-monitor" and "0"
201
201
 
202
- // Determine if this is main meter or additional meter
203
- if (statePathParts.length === 3) {
204
- // Main meter: gas.billing.closePeriod
205
- const type = statePathParts[0];
206
- this.log.info(`User triggered billing period closure for ${type} (main meter)`);
207
- await this.closeBillingPeriod(type);
208
- } else if (statePathParts.length === 4) {
209
- // Additional meter: gas.erdgeschoss.billing.closePeriod
202
+ // All meters now use: gas.meterName.billing.closePeriod (length === 4)
203
+ if (statePathParts.length === 4) {
210
204
  const type = statePathParts[0];
211
205
  const meterName = statePathParts[1];
206
+
212
207
  this.log.info(`User triggered billing period closure for ${type}.${meterName}`);
213
208
 
214
209
  // Find the meter object from multiMeterManager
@@ -221,24 +216,42 @@ class UtilityMonitor extends utils.Adapter {
221
216
  this.log.error(`Meter "${meterName}" not found for type ${type}!`);
222
217
  await this.setStateAsync(`${type}.${meterName}.billing.closePeriod`, false, true);
223
218
  }
219
+ } else {
220
+ // Invalid path format
221
+ this.log.warn(`Invalid billing period closure trigger: ${id}`);
224
222
  }
225
223
  return;
226
224
  }
227
225
 
228
226
  // Check if this is an adjustment value change
227
+ // New structure: gas.meterName.adjustment.value (4 parts after namespace)
229
228
  if (id.includes('.adjustment.value') && !state.ack) {
230
229
  const parts = id.split('.');
231
- const type = parts[parts.length - 3];
232
- this.log.info(`Adjustment value changed for ${type}: ${state.val}`);
233
- await this.setStateAsync(`${type}.adjustment.applied`, Date.now(), true);
230
+ // Parse: nebenkosten-monitor.0.gas.meterName.adjustment.value
231
+ const statePathParts = parts.slice(2); // Remove "nebenkosten-monitor" and "0"
234
232
 
235
- // Update costs for all meters of this type
236
- await this.updateCosts(type);
237
- return;
233
+ if (statePathParts.length === 3) {
234
+ // New structure: gas.meterName.adjustment.value
235
+ const type = statePathParts[0];
236
+ const meterName = statePathParts[1];
237
+
238
+ this.log.info(`Adjustment value changed for ${type}.${meterName}: ${state.val}`);
239
+ await this.setStateAsync(`${type}.${meterName}.adjustment.applied`, Date.now(), true);
240
+
241
+ // Update costs for THIS specific meter
242
+ if (this.multiMeterManager) {
243
+ const meters = this.multiMeterManager.getMetersForType(type);
244
+ const meter = meters.find(m => m.name === meterName);
245
+ if (meter) {
246
+ await this.multiMeterManager.updateCosts(type, meterName, meter.config);
247
+ }
248
+ }
249
+ return;
250
+ }
238
251
  }
239
252
 
240
253
  // Determine which utility this sensor belongs to
241
- // First check if it's a multi-meter sensor (additional meters)
254
+ // All meters (including main) are now handled by multiMeterManager
242
255
  if (this.multiMeterManager) {
243
256
  const meterInfo = this.multiMeterManager.findMeterBySensor(id);
244
257
  if (meterInfo && typeof state.val === 'number') {
@@ -246,19 +259,6 @@ class UtilityMonitor extends utils.Adapter {
246
259
  return;
247
260
  }
248
261
  }
249
-
250
- // Check main meter sensors
251
- const types = ['gas', 'water', 'electricity', 'pv'];
252
- for (const type of types) {
253
- const configType = this.consumptionManager.getConfigType(type);
254
-
255
- if (this.config[`${configType}Aktiv`] && this.config[`${configType}SensorDP`] === id) {
256
- if (typeof state.val === 'number') {
257
- await this.handleSensorUpdate(type, id, state.val);
258
- }
259
- return;
260
- }
261
- }
262
262
  }
263
263
 
264
264
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.utility-monitor",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Monitor gas, water, and electricity consumption with cost calculation",
5
5
  "author": {
6
6
  "name": "fischi87",