iobroker.utility-monitor 1.4.2 → 1.4.4
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 +16 -85
- package/admin/jsonConfig.json +37 -3
- package/admin/tab_m.html +305 -0
- package/io-package.json +27 -40
- package/lib/billingManager.js +22 -2
- package/lib/consumptionManager.js +1 -1
- package/lib/importManager.js +344 -0
- package/lib/messagingHandler.js +62 -0
- package/lib/multiMeterManager.js +37 -32
- package/lib/stateManager.js +31 -0
- package/main.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,6 +230,22 @@ Der Adapter setzt Zähler automatisch zurück:
|
|
|
230
230
|
|
|
231
231
|
### **WORK IN PROGRESS**
|
|
232
232
|
|
|
233
|
+
### 1.4.4 (2026-01-18)
|
|
234
|
+
|
|
235
|
+
- **FIX:** 🐛 **lastYearStart Recalculation Bug** - Fixed incorrect month count in paidTotal:
|
|
236
|
+
- `lastYearStart` is now always recalculated from `contractStart` on adapter initialization
|
|
237
|
+
- Fixes cases where `lastYearStart` was set incorrectly (e.g., 01.01.2026 instead of contract date)
|
|
238
|
+
- Ensures `monthsSinceYearStart` is always calculated correctly based on actual contract date
|
|
239
|
+
- Resolves issue where `paidTotal` showed only 1 month payment instead of correct accumulated amount
|
|
240
|
+
|
|
241
|
+
### 1.4.3 (2026-01-18)
|
|
242
|
+
|
|
243
|
+
- **FIX:** 🐛 **Critical paidTotal Calculation Bug** - Fixed incorrect paidTotal after sensor updates:
|
|
244
|
+
- `paidTotal` was stored as string instead of timestamp, causing parsing errors in `updateCosts()`
|
|
245
|
+
- Changed `lastYearStart`, `lastMonthStart`, `lastDayStart` to store timestamps (number) instead of formatted strings
|
|
246
|
+
- Now correctly calculates `paidTotal = monthlyPayment × monthsSinceYearStart` for both adapter restart and sensor updates
|
|
247
|
+
- Backward compatible: existing string values auto-convert to timestamps on next update
|
|
248
|
+
|
|
233
249
|
### 1.4.2 (2026-01-18)
|
|
234
250
|
|
|
235
251
|
- **FIX:** 🔧 **TypeScript Errors Resolved** - All TypeScript compilation errors fixed:
|
|
@@ -290,91 +306,6 @@ Der Adapter setzt Zähler automatisch zurück:
|
|
|
290
306
|
- **IMPROVED:** 🔍 **Debug-Logging** - Hilfreiche Debug-Logs für Troubleshooting (nur in Debug-Modus sichtbar)
|
|
291
307
|
- **CLEANUP:** 🧹 Repository aufgeräumt - Alte Backup-Dateien und temporäre Scripts entfernt
|
|
292
308
|
|
|
293
|
-
### 1.3.5 (2026-01-11)
|
|
294
|
-
|
|
295
|
-
- **NEW:** **Monatlicher Status-Bericht** - Optionaler monatlicher Bericht per Benachrichtigung.
|
|
296
|
-
- **NEW:** Datenpunkte `statistics.lastDay` (Verbrauch gestern) für alle Typen und `lastDayVolume` (Gas) hinzugefügt.
|
|
297
|
-
- **FIX:** **PV-Reset Bug** behoben (Tages- und Monatswerte wurden nicht zurückgesetzt).
|
|
298
|
-
- **FIX:** Schema-Validierung für Preisfelder korrigiert (Fix für Kommastellen).
|
|
299
|
-
- **FIX:** HT/NT-Anzeige korrigiert.
|
|
300
|
-
- **IMPROVED:** **Admin-UI Info-Tab** komplett optimiert (Sauberes Markdown & Layout).
|
|
301
|
-
- **IMPROVED:** Einheitliche Rundung berechneter Werte auf **2 Nachkommastellen** (daily, monthly, yearly).
|
|
302
|
-
- **ROBUSTNESS:** ioBroker Bot Compliance Check (Grid-Attribute in Admin-UI vereinheitlicht).
|
|
303
|
-
|
|
304
|
-
### 1.3.4 (2026-01-10)
|
|
305
|
-
|
|
306
|
-
- **FIX:** Kritischer Fix: Kommastellen für Gebühren-Felder (Grundgebühr, Arbeitspreis) werden nun korrekt gespeichert (Erlaubt 4 Nachkommastellen).
|
|
307
|
-
|
|
308
|
-
### 1.3.3 (2026-01-09)
|
|
309
|
-
|
|
310
|
-
- **IMPROVED:** Konfigurations-Reihenfolge optimiert (Gebühren logisch gruppiert).
|
|
311
|
-
- **NEW:** **PV-Benachrichtigungen** - Erhalte Erinnerungen auch für deine PV-Anlage (Abrechnung/Vertrag).
|
|
312
|
-
|
|
313
|
-
### 1.3.2 (2026-01-09)
|
|
314
|
-
|
|
315
|
-
- **NEW:** **PV / Einspeise-Unterstützung** ☀️ - Neuer Tab für Photovoltaik:
|
|
316
|
-
- Überwache deine Netzeinspeisung (kWh).
|
|
317
|
-
- Berechne deine Vergütung (Earnings) automatisch.
|
|
318
|
-
- Volle Unterstützung für Zählerstände, Abrechnungszeiträume und Historie.
|
|
319
|
-
|
|
320
|
-
### 1.3.1 (2026-01-09)
|
|
321
|
-
|
|
322
|
-
- **FIX:** Kritischer Fehler behoben: HT/NT-Datenpunkte wurden aufgrund eines internen Namensfehlers (electricity vs. strom) nicht angelegt.
|
|
323
|
-
- **FIX:** Warnungen im Log "State ... has no existing object" beseitigt.
|
|
324
|
-
|
|
325
|
-
### 1.3.0 (2026-01-09)
|
|
326
|
-
|
|
327
|
-
- **NEW:** **Differenzierte Benachrichtigungen** - Zwei getrennte Erinnerungstypen:
|
|
328
|
-
- **Abrechnungsende**: Erinnerung zum Zählerstand ablesen (z.B. 7 Tage vorher).
|
|
329
|
-
- **Vertragswechsel**: Erinnerung zum Tarif-Check / Kündigen (z.B. 60 Tage vorher).
|
|
330
|
-
- **NEW:** **Interaktives Benachrichtigungs-Feedback** - Der Test-Button zeigt nun direkt Erfolgs- oder Fehlermeldungen via Popup an (inkl. SMTP-Fehler vom Email-Adapter).
|
|
331
|
-
- **NEW:** **Live-Test ohne Speichern** - Benachrichtigungen können jetzt sofort getestet werden, ohne die Konfiguration vorher speichern zu müssen.
|
|
332
|
-
- **NEW:** **Modularer Code-Aufbau** - Umstellung auf eine moderne Architektur mit spezialisierten Managern für bessere Performance und Wartbarkeit.
|
|
333
|
-
- **IMPROVED:** **Responsives Admin-UI** - Kompakteres Button-Design und optimierte Darstellung auf mobilen Geräten.
|
|
334
|
-
- **FIX:** Redundante Volumen-Datenpunkte (`dailyVolume` etc.) für Strom und Wasser entfernt, um Log-Warnungen zu vermeiden.
|
|
335
|
-
- **FIX:** Mandatory bot requirements (Changelog header, News cleanup).
|
|
336
|
-
|
|
337
|
-
### 1.2.7 (2026-01-08)
|
|
338
|
-
|
|
339
|
-
- **NEW:** Universelles Benachrichtigungssystem für Abrechnungszeitraum-Erinnerungen (Telegram, Pushover, Email, etc.)
|
|
340
|
-
- **NEW:** Optionale PayPal-Unterstützung (Links in README und Config)
|
|
341
|
-
- **FIX:** Dezimalstellen für Tagesverbrauch auf 3 erhöht (bessere Unterstützung für Sensoren mit kleinen Deltas wie Shelly)
|
|
342
|
-
- **FIX:** Erlauben von leeren Preisen/Gebühren in der Konfiguration (verhindert Speicher-Fehler)
|
|
343
|
-
|
|
344
|
-
### 1.2.6 (2026-01-08)
|
|
345
|
-
|
|
346
|
-
- **FIX:** Erlaube leere Felder für Grundgebühr/Jahresgebühr/Abschlag in der Konfiguration (verhindert Speicher-Block im Admin-UI)
|
|
347
|
-
|
|
348
|
-
### 1.2.5 (2026-01-08)
|
|
349
|
-
|
|
350
|
-
- **NEW:** Transparente Anzeige des Vertragsbeginns bei jedem Adapter-Start im Log
|
|
351
|
-
- **NEW:** Unterstützung für zusätzliche **Jahresgebühren** (z.B. Zählermiete)
|
|
352
|
-
- **NEW:** Datenpunkt `costs.totalYearly` für die echten Gesamtkosten
|
|
353
|
-
- **FIX:** Kritischer Fehler in der Verbrauchs-Delta-Berechnung behoben (v1.2.4)
|
|
354
|
-
- **FIX:** Arbeitspreis-Anzeige bei Strom korrigiert
|
|
355
|
-
- **FIX:** Gas m³ → kWh Umrechnung für Anpassungswerte
|
|
356
|
-
- **FIX:** Korrekte Initialisierung des Vertragsjahres bei Neustart
|
|
357
|
-
- **FIX:** Vereinheitlichung der Konfigurationsschlüssel (`wasserInitialReading`)
|
|
358
|
-
- **ROBUSTNESS:** Schutz vor Datenverlust bei Adapter-Neustart (Zählerstand-Persistierung)
|
|
359
|
-
- **ROBUSTNESS:** Integration von manuellen Anpassungen in die HT/NT-Kostenrechnung
|
|
360
|
-
- **NEW:** Volle Unterstützung für **HT/NT-Tarife** für alle Energieträger (Strom, Gas, Wasser)
|
|
361
|
-
- **NEW:** Automatische Archivierung von HT/NT-Verbräuchen und Kosten in der Historie
|
|
362
|
-
- **DOCS:** Internationalisierung von Titel und Beschreibung
|
|
363
|
-
|
|
364
|
-
### 1.2.2 (2026-01-08)
|
|
365
|
-
|
|
366
|
-
- **NEW:** Manuelle Anpassung für Sensor-Abdrift-Korrektur
|
|
367
|
-
- **NEW:** Abrechnungszeitraum-Management mit automatischer Archivierung
|
|
368
|
-
- **NEW:** Unterstützung für zusätzliche **Jahresgebühren** (z.B. Zählermiete)
|
|
369
|
-
- **NEW:** Datenpunkt `costs.totalYearly` für die echten Gesamtkosten
|
|
370
|
-
- **FIX:** Arbeitspreis-Anzeige bei Strom korrigiert
|
|
371
|
-
- **FIX:** Gas m³ → kWh Umrechnung für Anpassungswerte
|
|
372
|
-
- **DOCS:** Internationalisierung von Titel und Beschreibung
|
|
373
|
-
|
|
374
|
-
---
|
|
375
|
-
|
|
376
|
-
- Initial release
|
|
377
|
-
|
|
378
309
|
---
|
|
379
310
|
|
|
380
311
|
## License
|
package/admin/jsonConfig.json
CHANGED
|
@@ -1455,18 +1455,52 @@
|
|
|
1455
1455
|
}
|
|
1456
1456
|
}
|
|
1457
1457
|
},
|
|
1458
|
+
"tabImport": {
|
|
1459
|
+
"type": "panel",
|
|
1460
|
+
"label": "📥 Import",
|
|
1461
|
+
"items": {
|
|
1462
|
+
"_importInfo": {
|
|
1463
|
+
"type": "header",
|
|
1464
|
+
"text": "CSV-Daten importieren",
|
|
1465
|
+
"size": 3
|
|
1466
|
+
},
|
|
1467
|
+
"_importLink": {
|
|
1468
|
+
"type": "staticLink",
|
|
1469
|
+
"label": "🚀 Import-Seite öffnen",
|
|
1470
|
+
"button": true,
|
|
1471
|
+
"variant": "contained",
|
|
1472
|
+
"color": "primary",
|
|
1473
|
+
"href": "/adapter/utility-monitor/tab-import.html",
|
|
1474
|
+
"target": "_blank",
|
|
1475
|
+
"sm": 12,
|
|
1476
|
+
"md": 6,
|
|
1477
|
+
"lg": 4
|
|
1478
|
+
},
|
|
1479
|
+
"_importDescription": {
|
|
1480
|
+
"type": "staticText",
|
|
1481
|
+
"text": "Klicke auf den Button oben, um die Import-Seite in einem neuen Tab zu öffnen. Dort kannst du CSV-Dateien mit historischen Zählerständen hochladen.",
|
|
1482
|
+
"newLine": true,
|
|
1483
|
+
"sm": 12,
|
|
1484
|
+
"style": {
|
|
1485
|
+
"fontSize": "0.9em",
|
|
1486
|
+
"color": "#666",
|
|
1487
|
+
"marginTop": "20px"
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1458
1492
|
"tabInfo": {
|
|
1459
1493
|
"type": "panel",
|
|
1460
1494
|
"label": "ℹ️ Info & Hilfe",
|
|
1461
1495
|
"items": {
|
|
1462
1496
|
"_infoHeader": {
|
|
1463
1497
|
"type": "header",
|
|
1464
|
-
"text": "
|
|
1498
|
+
"text": "Utility Monitor",
|
|
1465
1499
|
"size": 3
|
|
1466
1500
|
},
|
|
1467
1501
|
"_infoVersion": {
|
|
1468
1502
|
"type": "staticText",
|
|
1469
|
-
"text": "**Version:** 1.4.
|
|
1503
|
+
"text": "**Version:** 1.4.4\n\n**Autor:** fischi87",
|
|
1470
1504
|
"sm": 12,
|
|
1471
1505
|
"xs": 12,
|
|
1472
1506
|
"md": 12,
|
|
@@ -1475,7 +1509,7 @@
|
|
|
1475
1509
|
},
|
|
1476
1510
|
"donation": {
|
|
1477
1511
|
"type": "staticText",
|
|
1478
|
-
"text": "☕ [PayPal Spenden](https://paypal.me/bigplay87) | 📁 [GitHub Repository](https://github.com/fischi87/ioBroker.
|
|
1512
|
+
"text": "☕ [PayPal Spenden](https://paypal.me/bigplay87) | 📁 [GitHub Repository](https://github.com/fischi87/ioBroker.utility-monitor)",
|
|
1479
1513
|
"xs": 12,
|
|
1480
1514
|
"sm": 12,
|
|
1481
1515
|
"md": 12,
|
package/admin/tab_m.html
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<link rel="stylesheet" type="text/css" href="../../lib/css/materialize.css">
|
|
6
|
+
<link rel="stylesheet" type="text/css" href="../../css/adapter.css"/>
|
|
7
|
+
<script type="text/javascript" src="../../lib/js/jquery-3.2.1.min.js"></script>
|
|
8
|
+
<script type="text/javascript" src="../../socket.io/socket.io.js"></script>
|
|
9
|
+
<script type="text/javascript" src="../../js/adapter-settings.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
padding: 20px;
|
|
13
|
+
background: transparent;
|
|
14
|
+
}
|
|
15
|
+
.import-container {
|
|
16
|
+
max-width: 800px;
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
}
|
|
19
|
+
.section {
|
|
20
|
+
margin-bottom: 30px;
|
|
21
|
+
padding: 20px;
|
|
22
|
+
border-radius: 4px;
|
|
23
|
+
background: var(--color-background-box, #fff);
|
|
24
|
+
}
|
|
25
|
+
.section h2 {
|
|
26
|
+
margin-top: 0;
|
|
27
|
+
font-size: 1.5em;
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
}
|
|
30
|
+
.section h3 {
|
|
31
|
+
font-size: 1.2em;
|
|
32
|
+
font-weight: 500;
|
|
33
|
+
margin-bottom: 10px;
|
|
34
|
+
}
|
|
35
|
+
.help-text {
|
|
36
|
+
color: #666;
|
|
37
|
+
font-size: 0.9em;
|
|
38
|
+
margin-bottom: 15px;
|
|
39
|
+
}
|
|
40
|
+
.code-block {
|
|
41
|
+
background: #f5f5f5;
|
|
42
|
+
padding: 10px;
|
|
43
|
+
border-radius: 4px;
|
|
44
|
+
font-family: monospace;
|
|
45
|
+
white-space: pre;
|
|
46
|
+
margin: 10px 0;
|
|
47
|
+
}
|
|
48
|
+
.input-field {
|
|
49
|
+
margin-bottom: 20px;
|
|
50
|
+
}
|
|
51
|
+
.input-field label {
|
|
52
|
+
display: block;
|
|
53
|
+
margin-bottom: 5px;
|
|
54
|
+
font-weight: 500;
|
|
55
|
+
}
|
|
56
|
+
.input-field select,
|
|
57
|
+
.input-field input[type="file"] {
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: 8px;
|
|
60
|
+
border: 1px solid #ddd;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
}
|
|
63
|
+
.btn {
|
|
64
|
+
padding: 10px 20px;
|
|
65
|
+
border: none;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
font-size: 1em;
|
|
69
|
+
font-weight: 500;
|
|
70
|
+
}
|
|
71
|
+
.btn-primary {
|
|
72
|
+
background: #2196F3;
|
|
73
|
+
color: white;
|
|
74
|
+
}
|
|
75
|
+
.btn-primary:hover {
|
|
76
|
+
background: #1976D2;
|
|
77
|
+
}
|
|
78
|
+
.btn-primary:disabled {
|
|
79
|
+
background: #ccc;
|
|
80
|
+
cursor: not-allowed;
|
|
81
|
+
}
|
|
82
|
+
.message {
|
|
83
|
+
padding: 15px;
|
|
84
|
+
border-radius: 4px;
|
|
85
|
+
margin: 15px 0;
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
.message.success {
|
|
89
|
+
background: #4CAF50;
|
|
90
|
+
color: white;
|
|
91
|
+
}
|
|
92
|
+
.message.error {
|
|
93
|
+
background: #f44336;
|
|
94
|
+
color: white;
|
|
95
|
+
}
|
|
96
|
+
.message.info {
|
|
97
|
+
background: #2196F3;
|
|
98
|
+
color: white;
|
|
99
|
+
}
|
|
100
|
+
.spinner {
|
|
101
|
+
display: none;
|
|
102
|
+
margin-left: 10px;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
<div class="import-container">
|
|
108
|
+
<div class="section">
|
|
109
|
+
<h2>📥 CSV-Daten importieren</h2>
|
|
110
|
+
<p class="help-text">
|
|
111
|
+
Importiere historische Zählerstände aus einer CSV-Datei.
|
|
112
|
+
Die Daten werden unter <strong>history.csv</strong> gespeichert und überschreiben keine bestehenden Daten.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="section">
|
|
117
|
+
<h3>CSV-Format</h3>
|
|
118
|
+
<p class="help-text">Erwartetes Format:</p>
|
|
119
|
+
<div class="code-block">Datum;Zaehlerstand
|
|
120
|
+
01.01.2024;10250.5
|
|
121
|
+
01.02.2024;10285.3
|
|
122
|
+
01.03.2024;10320.8</div>
|
|
123
|
+
<ul class="help-text">
|
|
124
|
+
<li>Trennzeichen: Semikolon (<code>;</code>)</li>
|
|
125
|
+
<li>Dezimaltrennzeichen: Komma (<code>,</code>) oder Punkt (<code>.</code>)</li>
|
|
126
|
+
<li>Datumsformat: <code>DD.MM.YYYY</code> oder <code>DD.MM.YY</code></li>
|
|
127
|
+
<li>Der Verbrauch wird automatisch aus der Differenz berechnet</li>
|
|
128
|
+
</ul>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="section">
|
|
132
|
+
<h3>Import durchführen</h3>
|
|
133
|
+
|
|
134
|
+
<div class="input-field">
|
|
135
|
+
<label for="medium">Medium auswählen:</label>
|
|
136
|
+
<select id="medium">
|
|
137
|
+
<option value="">-- Bitte wählen --</option>
|
|
138
|
+
<option value="gas">🔥 Gas</option>
|
|
139
|
+
<option value="water">💧 Wasser</option>
|
|
140
|
+
<option value="electricity">⚡ Strom</option>
|
|
141
|
+
<option value="pv">☀️ PV</option>
|
|
142
|
+
</select>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="input-field">
|
|
146
|
+
<label for="csvFile">CSV-Datei auswählen:</label>
|
|
147
|
+
<input type="file" id="csvFile" accept=".csv,.txt" />
|
|
148
|
+
<p class="help-text">Wähle eine CSV-Datei von deinem Computer aus (max. 1 MB)</p>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<button id="importBtn" class="btn btn-primary" disabled>
|
|
152
|
+
📤 CSV importieren
|
|
153
|
+
<span class="spinner">⏳</span>
|
|
154
|
+
</button>
|
|
155
|
+
|
|
156
|
+
<div id="message" class="message"></div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="section">
|
|
160
|
+
<h3>Nach dem Import</h3>
|
|
161
|
+
<p class="help-text">Die importierten Daten findest du unter:</p>
|
|
162
|
+
<ul class="help-text">
|
|
163
|
+
<li><code>{medium}.history.csv.{Jahr}.{Monat}.reading</code> - Zählerstand</li>
|
|
164
|
+
<li><code>{medium}.history.csv.{Jahr}.{Monat}.consumption</code> - Verbrauch</li>
|
|
165
|
+
<li><code>{medium}.history.csv.{Jahr}.{Monat}.date</code> - Ablesedatum</li>
|
|
166
|
+
</ul>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<script>
|
|
171
|
+
let socket;
|
|
172
|
+
let instance;
|
|
173
|
+
|
|
174
|
+
// Initialize socket connection
|
|
175
|
+
function initSocket() {
|
|
176
|
+
socket = io.connect('/', {
|
|
177
|
+
path: '/socket.io',
|
|
178
|
+
reconnection: true
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
socket.on('connect', function() {
|
|
182
|
+
console.log('Socket connected');
|
|
183
|
+
|
|
184
|
+
// Get instance number from URL
|
|
185
|
+
const match = window.location.search.match(/instance=(\d+)/);
|
|
186
|
+
instance = match ? parseInt(match[1]) : 0;
|
|
187
|
+
console.log('Instance:', instance);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
socket.on('disconnect', function() {
|
|
191
|
+
console.log('Socket disconnected');
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Enable/disable import button based on form validity
|
|
196
|
+
function updateImportButton() {
|
|
197
|
+
const medium = $('#medium').val();
|
|
198
|
+
const file = $('#csvFile')[0].files[0];
|
|
199
|
+
$('#importBtn').prop('disabled', !medium || !file);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Show message
|
|
203
|
+
function showMessage(text, type = 'info') {
|
|
204
|
+
const $msg = $('#message');
|
|
205
|
+
$msg.removeClass('success error info');
|
|
206
|
+
$msg.addClass(type);
|
|
207
|
+
$msg.text(text);
|
|
208
|
+
$msg.show();
|
|
209
|
+
|
|
210
|
+
if (type === 'success' || type === 'error') {
|
|
211
|
+
setTimeout(() => $msg.fadeOut(), 5000);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Hide message
|
|
216
|
+
function hideMessage() {
|
|
217
|
+
$('#message').hide();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Import CSV
|
|
221
|
+
function importCSV() {
|
|
222
|
+
const medium = $('#medium').val();
|
|
223
|
+
const file = $('#csvFile')[0].files[0];
|
|
224
|
+
|
|
225
|
+
if (!medium || !file) {
|
|
226
|
+
showMessage('Bitte Medium und Datei auswählen!', 'error');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check file size (max 1 MB)
|
|
231
|
+
if (file.size > 1048576) {
|
|
232
|
+
showMessage('Datei ist zu groß! Maximum: 1 MB', 'error');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Show loading
|
|
237
|
+
$('#importBtn').prop('disabled', true);
|
|
238
|
+
$('.spinner').show();
|
|
239
|
+
showMessage('CSV wird importiert...', 'info');
|
|
240
|
+
|
|
241
|
+
// Read file
|
|
242
|
+
const reader = new FileReader();
|
|
243
|
+
reader.onload = function(e) {
|
|
244
|
+
const content = e.target.result;
|
|
245
|
+
|
|
246
|
+
console.log('Sending import request:', {
|
|
247
|
+
medium: medium,
|
|
248
|
+
fileLength: content.length
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Send to adapter
|
|
252
|
+
socket.emit('sendTo', 'utility-monitor.' + instance, 'importCSV', {
|
|
253
|
+
medium: medium,
|
|
254
|
+
file: content
|
|
255
|
+
}, function(result) {
|
|
256
|
+
console.log('Import result:', result);
|
|
257
|
+
|
|
258
|
+
$('#importBtn').prop('disabled', false);
|
|
259
|
+
$('.spinner').hide();
|
|
260
|
+
|
|
261
|
+
if (result.error) {
|
|
262
|
+
showMessage('❌ Fehler: ' + result.error, 'error');
|
|
263
|
+
} else if (result.success) {
|
|
264
|
+
showMessage(
|
|
265
|
+
`✅ Import erfolgreich! ${result.imported} von ${result.total} Einträgen importiert. ` +
|
|
266
|
+
`Jahre: ${result.years.join(', ')}`,
|
|
267
|
+
'success'
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Reset form
|
|
271
|
+
$('#csvFile').val('');
|
|
272
|
+
updateImportButton();
|
|
273
|
+
} else {
|
|
274
|
+
showMessage('❌ Unbekannter Fehler beim Import', 'error');
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
reader.onerror = function() {
|
|
280
|
+
$('#importBtn').prop('disabled', false);
|
|
281
|
+
$('.spinner').hide();
|
|
282
|
+
showMessage('❌ Fehler beim Lesen der Datei', 'error');
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
reader.readAsText(file);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Document ready
|
|
289
|
+
$(document).ready(function() {
|
|
290
|
+
console.log('Import page loaded');
|
|
291
|
+
|
|
292
|
+
// Initialize socket
|
|
293
|
+
initSocket();
|
|
294
|
+
|
|
295
|
+
// Event listeners
|
|
296
|
+
$('#medium').on('change', updateImportButton);
|
|
297
|
+
$('#csvFile').on('change', updateImportButton);
|
|
298
|
+
$('#importBtn').on('click', importCSV);
|
|
299
|
+
|
|
300
|
+
// Initial button state
|
|
301
|
+
updateImportButton();
|
|
302
|
+
});
|
|
303
|
+
</script>
|
|
304
|
+
</body>
|
|
305
|
+
</html>
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "utility-monitor",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.4.4": {
|
|
7
|
+
"en": "Fix: lastYearStart now always recalculated from contract date on adapter start - fixes wrong month count in paidTotal",
|
|
8
|
+
"de": "Fix: lastYearStart wird jetzt immer aus Vertragsbeginn neu berechnet - behebt falsche Monatszahl in paidTotal",
|
|
9
|
+
"ru": "Исправление: lastYearStart теперь всегда пересчитывается из даты договора при запуске - исправляет неправильный подсчет месяцев в paidTotal",
|
|
10
|
+
"pt": "Correção: lastYearStart agora sempre recalculado da data do contrato na inicialização - corrige contagem incorreta de meses em paidTotal",
|
|
11
|
+
"nl": "Fix: lastYearStart nu altijd herberekend vanaf contractdatum bij start - lost onjuiste maandtelling in paidTotal op",
|
|
12
|
+
"fr": "Correction: lastYearStart maintenant toujours recalculé à partir de la date du contrat au démarrage - corrige le comptage incorrect des mois dans paidTotal",
|
|
13
|
+
"it": "Correzione: lastYearStart ora sempre ricalcolato dalla data contratto all'avvio - corregge conteggio errato mesi in paidTotal",
|
|
14
|
+
"es": "Corrección: lastYearStart ahora siempre recalculado desde fecha contrato al iniciar - corrige recuento incorrecto meses en paidTotal",
|
|
15
|
+
"pl": "Naprawa: lastYearStart teraz zawsze przeliczany z daty umowy przy starcie - naprawia błędne liczenie miesięcy w paidTotal",
|
|
16
|
+
"uk": "Виправлення: lastYearStart тепер завжди перераховується з дати договору при запуску - виправляє неправильний підрахунок місяців у paidTotal",
|
|
17
|
+
"zh-cn": "修复:lastYearStart现在在启动时始终从合同日期重新计算 - 修复paidTotal中的错误月数计数"
|
|
18
|
+
},
|
|
19
|
+
"1.4.3": {
|
|
20
|
+
"en": "Fix: Critical paidTotal calculation bug - now correctly calculates (monthly payment × months) after sensor updates",
|
|
21
|
+
"de": "Fix: Kritischer paidTotal-Berechnungsfehler - berechnet jetzt korrekt (monatlicher Abschlag × Monate) nach Sensor-Updates",
|
|
22
|
+
"ru": "Исправление: Критическая ошибка расчета paidTotal - теперь правильно вычисляет (ежемесячный платеж × месяцы) после обновлений датчика",
|
|
23
|
+
"pt": "Correção: Bug crítico de cálculo paidTotal - agora calcula corretamente (pagamento mensal × meses) após atualizações do sensor",
|
|
24
|
+
"nl": "Fix: Kritieke paidTotal-berekeningsfout - berekent nu correct (maandelijkse betaling × maanden) na sensor-updates",
|
|
25
|
+
"fr": "Correction: Bug critique de calcul paidTotal - calcule maintenant correctement (paiement mensuel × mois) après les mises à jour du capteur",
|
|
26
|
+
"it": "Correzione: Bug critico calcolo paidTotal - ora calcola correttamente (pagamento mensile × mesi) dopo aggiornamenti sensore",
|
|
27
|
+
"es": "Corrección: Bug crítico de cálculo paidTotal - ahora calcula correctamente (pago mensual × meses) después de actualizaciones del sensor",
|
|
28
|
+
"pl": "Naprawa: Krytyczny błąd obliczania paidTotal - teraz poprawnie oblicza (płatność miesięczna × miesiące) po aktualizacjach czujnika",
|
|
29
|
+
"uk": "Виправлення: Критична помилка розрахунку paidTotal - тепер правильно обчислює (щомісячний платіж × місяці) після оновлень датчика",
|
|
30
|
+
"zh-cn": "修复:关键的paidTotal计算错误 - 现在在传感器更新后正确计算(月付款×月数)"
|
|
31
|
+
},
|
|
6
32
|
"1.4.2": {
|
|
7
33
|
"en": "Fix: Critical multi-meter balance bug (hardcoded 12 months). Fix: TypeScript errors resolved. New: Enhanced input validation. New: Extended constants. New: Error handling wrapper.",
|
|
8
34
|
"de": "Fix: Kritischer Multi-Meter Balance-Bug (hardcodierte 12 Monate). Fix: TypeScript-Fehler behoben. Neu: Erweiterte Eingabevalidierung. Neu: Erweiterte Konstanten. Neu: Fehlerbehandlungs-Wrapper.",
|
|
@@ -41,45 +67,6 @@
|
|
|
41
67
|
"pl": "Nowe: Obsługa Wielu Liczników! Dodaj nieograniczoną liczbę niestandardowych liczników dla każdego typu mediów (gaz, woda, prąd). Automatyczne obliczanie sum. Pełna kompatybilność wsteczna.",
|
|
42
68
|
"uk": "Нове: Підтримка кількох лічильників! Додайте необмежену кількість власних лічильників для кожного типу комунальних послуг (газ, вода, електрика). Автоматичний розрахунок підсумків. Повна зворотна сумісність.",
|
|
43
69
|
"zh-cn": "新增:多表支持!为每种公用事业类型(燃气、水、电)添加无限的自定义计量表。自动总计计算。完全向后兼容。"
|
|
44
|
-
},
|
|
45
|
-
"1.3.5": {
|
|
46
|
-
"en": "New: Monthly Status Report. New: Added lastDay statistics. Fix: PV period reset bug. Fix: Schema validation for prices. Improved: Admin UI Info-Tab optimized. Improved: Consistent rounding (2 decimals). Improved: Bot compliance grid attributes.",
|
|
47
|
-
"de": "Neu: Monatlicher Status-Bericht. Neu: lastDay-Statistiken hinzugefügt. Fix: PV Perioden-Reset Fehler behoben. Fix: Schema-Validierung für Preisfelder. Verbessert: Admin-UI Info-Tab optimiert. Verbessert: Einheitliche Rundung (2 Stellen). Verbessert: Bot-Compliance Grid-Attribute.",
|
|
48
|
-
"ru": "Новое: Ежемесячный отчет о состоянии. Новое: Добавлена статистика lastDay. Исправление: Ошибка сброса периода PV. Исправление: Проверка схемы для цен. Улучшено: Оптимизирована вкладка информации Admin UI. Улучшено: Последовательное округление (2 знака). Улучшено: Атрибуты сетки соответствия ботов.",
|
|
49
|
-
"pt": "Novo: Relatório de Status Mensal. Novo: Adicionadas estatísticas lastDay. Correção: Bug de redefinição de período PV. Correção: Validação de esquema para preços. Melhorado: Guia de informações da Admin UI otimizada. Melhorado: Arredondamento consistente (2 decimais). Melhorado: Atributos de grade de conformidade do bot.",
|
|
50
|
-
"nl": "Nieuw: Maandelijks statusrapport. Nieuw: lastDay statistieken toegevoegd. Fix: PV periode reset bug. Fix: Schema validatie voor prijzen. Verbeterd: Admin UI Info-Tab geoptimaliseerd. Verbeterd: Consistente afronding (2 decimalen). Verbeterd: Bot compliance grid attributen.",
|
|
51
|
-
"fr": "Nouveau: Rapport d'état mensuel. Nouveau: Ajout de statistiques lastDay. Correction: Bug de réinitialisation de période PV. Correction: Validation de schéma pour les prix. Amélioré: Onglet d'informations Admin UI optimisé. Amélioré: Arrondi cohérent (2 décimales). Amélioré: Attributs de grille de conformité des bots.",
|
|
52
|
-
"it": "Nuovo: Rapporto di stato mensile. Nuovo: Aggiunte statistiche lastDay. Correzione: Bug di reset periodo PV. Correzione: Convalida dello schema per i prezzi. Migliorato: Scheda informazioni Admin UI ottimizzata. Migliorato: Arrotondamento coerente (2 decimali). Migliorato: Attributi griglia conformità bot.",
|
|
53
|
-
"es": "Nuevo: Informe de Estado Mensual. Nuevo: Agregadas estadísticas lastDay. Corrección: Error de reinicio de período PV. Corrección: Validación de esquema para precios. Mejorado: Pestaña de información de Admin UI optimizada. Mejorado: Redondeo consistente (2 decimales). Mejorado: Atributos de cuadrícula de cumplimiento de bots.",
|
|
54
|
-
"pl": "Nowe: Miesięczny raport statusu. Nowe: Dodano statystyki lastDay. Naprawa: Błąd resetowania okresu PV. Naprawa: Walidacja schematu dla cen. Ulepszone: Zoptymalizowana zakładka informacji Admin UI. Ulepszone: Spójne zaokrąglanie (2 miejsca dziesiętne). Ulepszone: Atrybuty siatki zgodności botów.",
|
|
55
|
-
"uk": "Нове: Щомісячний звіт про стан. Нове: Додано статистику lastDay. Виправлення: Помилка скидання періоду PV. Виправлення: Перевірка схеми для цін. Покращено: Оптимізовано вкладку інформації Admin UI. Покращено: Послідовне округлення (2 знаки). Покращено: Атрибути сітки відповідності ботів.",
|
|
56
|
-
"zh-cn": "新增:每月状态报告。新增:添加了lastDay统计信息。修复:PV周期重置错误。修复:价格架构验证。改进:优化了Admin UI信息选项卡。改进:一致的四舍五入(2位小数)。改进:机器人合规网格属性。"
|
|
57
|
-
},
|
|
58
|
-
"1.3.3": {
|
|
59
|
-
"en": "New: PV/Feed-in integration! Monitor your solar feed-in and earnings. New: Notifications for PV. Improved: Reorganized config UI.",
|
|
60
|
-
"de": "Neu: PV/Einspeise-Integration! Überwache deine Solareinspeisung und Vergütung. Neu: Benachrichtigungen für PV. Verbessert: Aufgeräumte Konfigurations-Oberfläche.",
|
|
61
|
-
"ru": "Новое: Интеграция PV/Feed-in! Отслеживайте свою солнечную подачу и доходы. Новое: Уведомления для PV. Улучшено: Реорганизован интерфейс конфигурации.",
|
|
62
|
-
"pt": "Novo: Integração PV/Feed-in! Monitore sua alimentação solar e ganhos. Novo: Notificações para PV. Melhorado: IU de configuração reorganizada.",
|
|
63
|
-
"nl": "Nieuw: PV/Feed-in integratie! Monitor je zonne-invoeding en inkomsten. Nieuw: Meldingen voor PV. Verbeterd: Gereorganiseerde configuratie-UI.",
|
|
64
|
-
"fr": "Nouveau: Intégration PV/Feed-in! Surveillez votre injection solaire et vos revenus. Nouveau: Notifications pour PV. Amélioré: Interface de configuration réorganisée.",
|
|
65
|
-
"it": "Nuovo: Integrazione PV/Feed-in! Monitora il tuo immissione solare e i guadagni. Nuovo: Notifiche per PV. Migliorato: IU di configurazione riorganizzata.",
|
|
66
|
-
"es": "Nuevo: Integración PV/Feed-in! Monitorea tu alimentación solar y ganancias. Nuevo: Notificaciones para PV. Mejorado: IU de configuración reorganizada.",
|
|
67
|
-
"pl": "Nowe: Integracja PV/Feed-in! Monitoruj swoje zasilanie słoneczne i zarobki. Nowe: Powiadomienia dla PV. Ulepszone: Zreorganizowany interfejs konfiguracji.",
|
|
68
|
-
"uk": "Нове: Інтеграція PV/Feed-in! Відстежуйте своє сонячне живлення та доходи. Нове: Сповіщення для PV. Покращено: Реорганізований інтерфейс конфігурації.",
|
|
69
|
-
"zh-cn": "新增:PV/Feed-in集成!监控您的太阳能馈入和收益。新增:PV通知。改进:重新组织的配置界面。"
|
|
70
|
-
},
|
|
71
|
-
"1.3.2": {
|
|
72
|
-
"en": "New: Initial support for PV integration.",
|
|
73
|
-
"de": "Neu: Erste Unterstützung für PV-Integration.",
|
|
74
|
-
"ru": "Новое: Начальная поддержка интеграции PV.",
|
|
75
|
-
"pt": "Novo: Suporte inicial para integração PV.",
|
|
76
|
-
"nl": "Nieuw: Initiële ondersteuning voor PV-integratie.",
|
|
77
|
-
"fr": "Nouveau: Support initial pour l'intégration PV.",
|
|
78
|
-
"it": "Nuovo: Supporto iniziale per l'integrazione PV.",
|
|
79
|
-
"es": "Nuevo: Soporte inicial para integración PV.",
|
|
80
|
-
"pl": "Nowe: Wstępne wsparcie dla integracji PV.",
|
|
81
|
-
"uk": "Нове: Початкова підтримка інтеграції PV.",
|
|
82
|
-
"zh-cn": "新增:PV集成的初始支持。"
|
|
83
70
|
}
|
|
84
71
|
},
|
|
85
72
|
"titleLang": {
|
package/lib/billingManager.js
CHANGED
|
@@ -438,7 +438,7 @@ class BillingManager {
|
|
|
438
438
|
const configType = this.adapter.consumptionManager.getConfigType(type);
|
|
439
439
|
contractStartDate = this.adapter.config[`${configType}ContractStart`];
|
|
440
440
|
} else {
|
|
441
|
-
contractStartDate = meter.config?.
|
|
441
|
+
contractStartDate = meter.config?.contractStart;
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
if (!contractStartDate) {
|
|
@@ -587,7 +587,7 @@ class BillingManager {
|
|
|
587
587
|
contractStartDate = this.adapter.config[`${configType}ContractStart`];
|
|
588
588
|
} else {
|
|
589
589
|
// Additional meter: use meter's individual config
|
|
590
|
-
contractStartDate = meter.config?.
|
|
590
|
+
contractStartDate = meter.config?.contractStart;
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
if (contractStartDate) {
|
|
@@ -665,6 +665,18 @@ class BillingManager {
|
|
|
665
665
|
await this.adapter.setStateAsync(`${basePath}.consumption.dailyVolume`, 0, true);
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
// Reset HT/NT daily counters if enabled
|
|
669
|
+
const configType = this.adapter.consumptionManager.getConfigType(type);
|
|
670
|
+
const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
|
|
671
|
+
if (htNtEnabled) {
|
|
672
|
+
const dailyHT = await this.adapter.getStateAsync(`${basePath}.consumption.dailyHT`);
|
|
673
|
+
const dailyNT = await this.adapter.getStateAsync(`${basePath}.consumption.dailyNT`);
|
|
674
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastDayHT`, dailyHT?.val || 0, true);
|
|
675
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastDayNT`, dailyNT?.val || 0, true);
|
|
676
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.dailyHT`, 0, true);
|
|
677
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.dailyNT`, 0, true);
|
|
678
|
+
}
|
|
679
|
+
|
|
668
680
|
await this.adapter.setStateAsync(`${basePath}.costs.daily`, 0, true);
|
|
669
681
|
|
|
670
682
|
// Update lastDayStart timestamp
|
|
@@ -715,6 +727,14 @@ class BillingManager {
|
|
|
715
727
|
await this.adapter.setStateAsync(`${basePath}.consumption.monthlyVolume`, 0, true);
|
|
716
728
|
}
|
|
717
729
|
|
|
730
|
+
// Reset HT/NT monthly counters if enabled
|
|
731
|
+
const configType = this.adapter.consumptionManager.getConfigType(type);
|
|
732
|
+
const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
|
|
733
|
+
if (htNtEnabled) {
|
|
734
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.monthlyHT`, 0, true);
|
|
735
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.monthlyNT`, 0, true);
|
|
736
|
+
}
|
|
737
|
+
|
|
718
738
|
await this.adapter.setStateAsync(`${basePath}.costs.monthly`, 0, true);
|
|
719
739
|
|
|
720
740
|
// Update lastMonthStart timestamp
|
|
@@ -244,7 +244,7 @@ class ConsumptionManager {
|
|
|
244
244
|
const lastValue = this.lastSensorValues[sensorDP];
|
|
245
245
|
this.lastSensorValues[sensorDP] = consumption;
|
|
246
246
|
|
|
247
|
-
if (lastValue === undefined || consumption
|
|
247
|
+
if (lastValue === undefined || consumption < lastValue) {
|
|
248
248
|
if (lastValue !== undefined && consumption < lastValue) {
|
|
249
249
|
this.adapter.log.warn(
|
|
250
250
|
`${type}: Sensor value decreased (${lastValue} -> ${consumption}). Assuming meter reset or replacement.`,
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImportHandler - Handles CSV import of historical meter readings
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Parse CSV files with date and meter readings
|
|
6
|
+
* - Automatically calculate consumption from reading differences
|
|
7
|
+
* - Create states under history.csv.{year}.{month}
|
|
8
|
+
* - Support for all utility types (gas, water, electricity, pv)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class ImportManager {
|
|
12
|
+
constructor(adapter, calculator) {
|
|
13
|
+
this.adapter = adapter;
|
|
14
|
+
this.calculator = calculator;
|
|
15
|
+
|
|
16
|
+
this.monthNames = [
|
|
17
|
+
'january', 'february', 'march', 'april', 'may', 'june',
|
|
18
|
+
'july', 'august', 'september', 'october', 'november', 'december'
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Main entry point for CSV import
|
|
24
|
+
* @param {string} medium - Utility type (gas, water, electricity, pv)
|
|
25
|
+
* @param {string} content - CSV file content
|
|
26
|
+
* @returns {Promise<object>} Import result
|
|
27
|
+
*/
|
|
28
|
+
async importCSV(medium, content) {
|
|
29
|
+
try {
|
|
30
|
+
this.adapter.log.info(`Starting CSV import for ${medium}...`);
|
|
31
|
+
|
|
32
|
+
// Validate medium
|
|
33
|
+
if (!['gas', 'water', 'electricity', 'pv'].includes(medium)) {
|
|
34
|
+
throw new Error(`Ungültiges Medium: ${medium}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Parse CSV
|
|
38
|
+
const entries = this.parseCSV(content);
|
|
39
|
+
|
|
40
|
+
if (entries.length === 0) {
|
|
41
|
+
throw new Error('Keine gültigen Einträge in der CSV gefunden');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.adapter.log.info(`Parsed ${entries.length} entries from CSV`);
|
|
45
|
+
|
|
46
|
+
// Group by year and month
|
|
47
|
+
const grouped = this.groupByYearMonth(entries);
|
|
48
|
+
|
|
49
|
+
// Create states
|
|
50
|
+
const importedCount = await this.createStates(medium, grouped);
|
|
51
|
+
|
|
52
|
+
this.adapter.log.info(`✅ Import erfolgreich: ${importedCount} Einträge importiert`);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
imported: importedCount,
|
|
57
|
+
total: entries.length,
|
|
58
|
+
years: Object.keys(grouped)
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.adapter.log.error(`CSV Import Fehler: ${error.message}`);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse CSV content
|
|
69
|
+
* Expected format:
|
|
70
|
+
* Datum;Zaehlerstand
|
|
71
|
+
* 01.01.2024;10250.5
|
|
72
|
+
* 01.02.2024;10285.3
|
|
73
|
+
*
|
|
74
|
+
* Also supports comma as decimal separator: 10250,5
|
|
75
|
+
*/
|
|
76
|
+
parseCSV(content) {
|
|
77
|
+
const lines = content.trim().split('\n');
|
|
78
|
+
|
|
79
|
+
if (lines.length < 2) {
|
|
80
|
+
throw new Error('CSV muss mindestens 2 Zeilen haben (Header + Daten)');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Parse header
|
|
84
|
+
const header = lines[0].toLowerCase().split(';').map(h => h.trim());
|
|
85
|
+
const dateIndex = header.findIndex(h => h === 'datum' || h === 'date');
|
|
86
|
+
const readingIndex = header.findIndex(h =>
|
|
87
|
+
h === 'zaehlerstand' || h === 'zählerstand' || h === 'reading' || h === 'stand'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (dateIndex === -1) {
|
|
91
|
+
throw new Error('CSV muss eine "Datum" Spalte enthalten');
|
|
92
|
+
}
|
|
93
|
+
if (readingIndex === -1) {
|
|
94
|
+
throw new Error('CSV muss eine "Zaehlerstand" Spalte enthalten');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const entries = [];
|
|
98
|
+
const errors = [];
|
|
99
|
+
|
|
100
|
+
for (let i = 1; i < lines.length; i++) {
|
|
101
|
+
const lineNum = i + 1;
|
|
102
|
+
const line = lines[i].trim();
|
|
103
|
+
|
|
104
|
+
if (!line) continue; // Skip empty lines
|
|
105
|
+
|
|
106
|
+
const values = line.split(';').map(v => v.trim());
|
|
107
|
+
|
|
108
|
+
if (values.length < Math.max(dateIndex, readingIndex) + 1) {
|
|
109
|
+
errors.push(`Zeile ${lineNum}: Nicht genügend Spalten`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dateStr = values[dateIndex];
|
|
114
|
+
const readingStr = values[readingIndex];
|
|
115
|
+
|
|
116
|
+
if (!dateStr || !readingStr) {
|
|
117
|
+
errors.push(`Zeile ${lineNum}: Datum oder Zählerstand fehlt`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Parse date (supports DD.MM.YYYY, DD.MM.YY, YYYY-MM-DD)
|
|
122
|
+
const date = this.parseDate(dateStr);
|
|
123
|
+
if (!date || isNaN(date.getTime())) {
|
|
124
|
+
errors.push(`Zeile ${lineNum}: Ungültiges Datum: ${dateStr}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parse reading (supports both , and . as decimal separator)
|
|
129
|
+
const reading = this.parseNumber(readingStr);
|
|
130
|
+
if (isNaN(reading) || reading < 0) {
|
|
131
|
+
errors.push(`Zeile ${lineNum}: Ungültiger Zählerstand: ${readingStr}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
entries.push({ date, reading, line: lineNum });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Log errors if any
|
|
139
|
+
if (errors.length > 0) {
|
|
140
|
+
this.adapter.log.warn(`Import-Warnungen:\n${errors.join('\n')}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Sort by date (oldest first)
|
|
144
|
+
entries.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
145
|
+
|
|
146
|
+
return entries;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse date string (supports multiple formats)
|
|
151
|
+
*/
|
|
152
|
+
parseDate(dateStr) {
|
|
153
|
+
// Try German format first (DD.MM.YYYY or DD.MM.YY)
|
|
154
|
+
const germanMatch = dateStr.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2,4})$/);
|
|
155
|
+
if (germanMatch) {
|
|
156
|
+
let day = parseInt(germanMatch[1]);
|
|
157
|
+
let month = parseInt(germanMatch[2]) - 1; // 0-indexed
|
|
158
|
+
let year = parseInt(germanMatch[3]);
|
|
159
|
+
|
|
160
|
+
// Handle 2-digit year
|
|
161
|
+
if (year < 100) {
|
|
162
|
+
year += year < 50 ? 2000 : 1900;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const date = new Date(year, month, day, 12, 0, 0);
|
|
166
|
+
return date;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try ISO format (YYYY-MM-DD)
|
|
170
|
+
const isoMatch = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
171
|
+
if (isoMatch) {
|
|
172
|
+
const year = parseInt(isoMatch[1]);
|
|
173
|
+
const month = parseInt(isoMatch[2]) - 1;
|
|
174
|
+
const day = parseInt(isoMatch[3]);
|
|
175
|
+
return new Date(year, month, day, 12, 0, 0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse number (supports comma and dot as decimal separator)
|
|
183
|
+
*/
|
|
184
|
+
parseNumber(str) {
|
|
185
|
+
// Replace comma with dot
|
|
186
|
+
const normalized = str.replace(',', '.');
|
|
187
|
+
return parseFloat(normalized);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Group entries by year and month, calculate consumption
|
|
192
|
+
*/
|
|
193
|
+
groupByYearMonth(entries) {
|
|
194
|
+
const grouped = {};
|
|
195
|
+
let previousReading = null;
|
|
196
|
+
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const year = entry.date.getFullYear();
|
|
199
|
+
const month = entry.date.getMonth();
|
|
200
|
+
const monthName = this.monthNames[month];
|
|
201
|
+
|
|
202
|
+
if (!grouped[year]) {
|
|
203
|
+
grouped[year] = {};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Calculate consumption (difference to previous reading)
|
|
207
|
+
const consumption = previousReading !== null ?
|
|
208
|
+
entry.reading - previousReading : null;
|
|
209
|
+
|
|
210
|
+
grouped[year][monthName] = {
|
|
211
|
+
date: entry.date,
|
|
212
|
+
reading: entry.reading,
|
|
213
|
+
consumption: consumption,
|
|
214
|
+
sourceLine: entry.line
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
previousReading = entry.reading;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return grouped;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create states for imported data
|
|
225
|
+
*/
|
|
226
|
+
async createStates(medium, grouped) {
|
|
227
|
+
let count = 0;
|
|
228
|
+
|
|
229
|
+
// Determine unit based on medium
|
|
230
|
+
const unit = this.getUnit(medium);
|
|
231
|
+
|
|
232
|
+
for (const [year, months] of Object.entries(grouped)) {
|
|
233
|
+
for (const [monthName, data] of Object.entries(months)) {
|
|
234
|
+
const basePath = `${medium}.history.csv.${year}.${monthName}`;
|
|
235
|
+
|
|
236
|
+
// Create reading state
|
|
237
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.reading`, {
|
|
238
|
+
type: 'state',
|
|
239
|
+
common: {
|
|
240
|
+
name: {
|
|
241
|
+
en: `Meter Reading ${monthName} ${year}`,
|
|
242
|
+
de: `Zählerstand ${monthName} ${year}`,
|
|
243
|
+
ru: `Показание счетчика ${monthName} ${year}`,
|
|
244
|
+
pt: `Leitura do Medidor ${monthName} ${year}`,
|
|
245
|
+
nl: `Meterstand ${monthName} ${year}`,
|
|
246
|
+
fr: `Relevé du Compteur ${monthName} ${year}`,
|
|
247
|
+
it: `Lettura Contatore ${monthName} ${year}`,
|
|
248
|
+
es: `Lectura del Medidor ${monthName} ${year}`,
|
|
249
|
+
pl: `Odczyt Licznika ${monthName} ${year}`,
|
|
250
|
+
uk: `Показник лічильника ${monthName} ${year}`,
|
|
251
|
+
'zh-cn': `仪表读数 ${monthName} ${year}`
|
|
252
|
+
},
|
|
253
|
+
type: 'number',
|
|
254
|
+
role: 'value',
|
|
255
|
+
read: true,
|
|
256
|
+
write: false,
|
|
257
|
+
unit: unit
|
|
258
|
+
},
|
|
259
|
+
native: {}
|
|
260
|
+
});
|
|
261
|
+
await this.adapter.setStateAsync(`${basePath}.reading`, data.reading, true);
|
|
262
|
+
|
|
263
|
+
// Create date state
|
|
264
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.date`, {
|
|
265
|
+
type: 'state',
|
|
266
|
+
common: {
|
|
267
|
+
name: {
|
|
268
|
+
en: `Date ${monthName} ${year}`,
|
|
269
|
+
de: `Datum ${monthName} ${year}`,
|
|
270
|
+
ru: `Дата ${monthName} ${year}`,
|
|
271
|
+
pt: `Data ${monthName} ${year}`,
|
|
272
|
+
nl: `Datum ${monthName} ${year}`,
|
|
273
|
+
fr: `Date ${monthName} ${year}`,
|
|
274
|
+
it: `Data ${monthName} ${year}`,
|
|
275
|
+
es: `Fecha ${monthName} ${year}`,
|
|
276
|
+
pl: `Data ${monthName} ${year}`,
|
|
277
|
+
uk: `Дата ${monthName} ${year}`,
|
|
278
|
+
'zh-cn': `日期 ${monthName} ${year}`
|
|
279
|
+
},
|
|
280
|
+
type: 'string',
|
|
281
|
+
role: 'date',
|
|
282
|
+
read: true,
|
|
283
|
+
write: false
|
|
284
|
+
},
|
|
285
|
+
native: {}
|
|
286
|
+
});
|
|
287
|
+
await this.adapter.setStateAsync(`${basePath}.date`,
|
|
288
|
+
this.calculator.formatDateString(data.date), true);
|
|
289
|
+
|
|
290
|
+
// Create consumption state (if calculable)
|
|
291
|
+
if (data.consumption !== null) {
|
|
292
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.consumption`, {
|
|
293
|
+
type: 'state',
|
|
294
|
+
common: {
|
|
295
|
+
name: {
|
|
296
|
+
en: `Consumption ${monthName} ${year}`,
|
|
297
|
+
de: `Verbrauch ${monthName} ${year}`,
|
|
298
|
+
ru: `Потребление ${monthName} ${year}`,
|
|
299
|
+
pt: `Consumo ${monthName} ${year}`,
|
|
300
|
+
nl: `Verbruik ${monthName} ${year}`,
|
|
301
|
+
fr: `Consommation ${monthName} ${year}`,
|
|
302
|
+
it: `Consumo ${monthName} ${year}`,
|
|
303
|
+
es: `Consumo ${monthName} ${year}`,
|
|
304
|
+
pl: `Zużycie ${monthName} ${year}`,
|
|
305
|
+
uk: `Споживання ${monthName} ${year}`,
|
|
306
|
+
'zh-cn': `消费 ${monthName} ${year}`
|
|
307
|
+
},
|
|
308
|
+
type: 'number',
|
|
309
|
+
role: 'value',
|
|
310
|
+
read: true,
|
|
311
|
+
write: false,
|
|
312
|
+
unit: unit
|
|
313
|
+
},
|
|
314
|
+
native: {}
|
|
315
|
+
});
|
|
316
|
+
await this.adapter.setStateAsync(`${basePath}.consumption`,
|
|
317
|
+
this.calculator.round(data.consumption, 2), true);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
count++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return count;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get unit for medium
|
|
329
|
+
*/
|
|
330
|
+
getUnit(medium) {
|
|
331
|
+
switch (medium) {
|
|
332
|
+
case 'gas':
|
|
333
|
+
case 'water':
|
|
334
|
+
return 'm³';
|
|
335
|
+
case 'electricity':
|
|
336
|
+
case 'pv':
|
|
337
|
+
return 'kWh';
|
|
338
|
+
default:
|
|
339
|
+
return '';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = ImportManager;
|
package/lib/messagingHandler.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const ImportManager = require('./importManager');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* MessagingHandler handles all incoming adapter messages
|
|
5
7
|
* and outgoing notifications.
|
|
@@ -10,6 +12,7 @@ class MessagingHandler {
|
|
|
10
12
|
*/
|
|
11
13
|
constructor(adapter) {
|
|
12
14
|
this.adapter = adapter;
|
|
15
|
+
this.importManager = null; // Lazy initialization
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -136,6 +139,65 @@ class MessagingHandler {
|
|
|
136
139
|
);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
142
|
+
} else if (obj.command === 'importCSV') {
|
|
143
|
+
this.adapter.log.info(`[importCSV] Received import request`);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
// Lazy initialize importManager
|
|
147
|
+
if (!this.importManager) {
|
|
148
|
+
const calculator = this.adapter.consumptionManager.calculator;
|
|
149
|
+
this.importManager = new ImportManager(this.adapter, calculator);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Extract data from obj.message, obj directly, or config (fallback)
|
|
153
|
+
// jsonConfig sometimes doesn't resolve ${data.xxx} variables in sendTo buttons
|
|
154
|
+
let medium = obj.message?.medium || obj.medium || this.adapter.config.importMedium;
|
|
155
|
+
let file = obj.message?.file || obj.file || this.adapter.config.importFileContent;
|
|
156
|
+
|
|
157
|
+
// Handle unresolved placeholders
|
|
158
|
+
if (medium && medium.includes('${data.')) {
|
|
159
|
+
medium = this.adapter.config.importMedium;
|
|
160
|
+
}
|
|
161
|
+
if (file && file.includes('${data.')) {
|
|
162
|
+
file = this.adapter.config.importFileContent;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.adapter.log.debug(`[importCSV] Medium: ${medium}, File length: ${file?.length || 0}`);
|
|
166
|
+
|
|
167
|
+
if (!medium || !file) {
|
|
168
|
+
this.adapter.sendTo(
|
|
169
|
+
obj.from,
|
|
170
|
+
obj.command,
|
|
171
|
+
{ error: 'Bitte Medium auswählen, CSV-Inhalt einfügen und dann SPEICHERN klicken, bevor du importierst!' },
|
|
172
|
+
obj.callback,
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// File comes as data URL (data:text/csv;base64,...)
|
|
178
|
+
let content;
|
|
179
|
+
if (file.includes('base64,')) {
|
|
180
|
+
// Extract base64 content
|
|
181
|
+
const base64Content = file.split('base64,')[1];
|
|
182
|
+
content = Buffer.from(base64Content, 'base64').toString('utf-8');
|
|
183
|
+
} else {
|
|
184
|
+
// Plain text
|
|
185
|
+
content = file;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Import the CSV
|
|
189
|
+
const result = await this.importManager.importCSV(medium, content);
|
|
190
|
+
|
|
191
|
+
this.adapter.sendTo(obj.from, obj.command, result, obj.callback);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.adapter.log.error(`[importCSV] Error: ${error.message}`);
|
|
194
|
+
this.adapter.sendTo(
|
|
195
|
+
obj.from,
|
|
196
|
+
obj.command,
|
|
197
|
+
{ error: error.message },
|
|
198
|
+
obj.callback,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
139
201
|
} else {
|
|
140
202
|
this.adapter.log.warn(`[onMessage] Unknown command: ${obj.command}`);
|
|
141
203
|
if (obj.callback) {
|
package/lib/multiMeterManager.js
CHANGED
|
@@ -221,43 +221,46 @@ class MultiMeterManager {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
// Initialize period start timestamps
|
|
224
|
-
const nowIso = calculator.formatDateString(new Date());
|
|
225
224
|
const timestampRoles = ['lastDayStart', 'lastMonthStart', 'lastYearStart'];
|
|
226
225
|
|
|
227
226
|
for (const role of timestampRoles) {
|
|
228
227
|
const statePath = `${basePath}.statistics.${role}`;
|
|
229
228
|
const state = await this.adapter.getStateAsync(statePath);
|
|
230
229
|
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (yearStartDate > now) {
|
|
248
|
-
yearStartDate.setFullYear(now.getFullYear() - 1);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
230
|
+
if (role === 'lastYearStart') {
|
|
231
|
+
// Calculate expected yearStart based on contract
|
|
232
|
+
const contractStart = calculator.parseGermanDate(config.contractStart);
|
|
233
|
+
let expectedYearStart;
|
|
234
|
+
|
|
235
|
+
if (contractStart && !isNaN(contractStart.getTime())) {
|
|
236
|
+
const now = new Date();
|
|
237
|
+
expectedYearStart = new Date(
|
|
238
|
+
now.getFullYear(),
|
|
239
|
+
contractStart.getMonth(),
|
|
240
|
+
contractStart.getDate(),
|
|
241
|
+
12,
|
|
242
|
+
0,
|
|
243
|
+
0,
|
|
244
|
+
);
|
|
251
245
|
|
|
252
|
-
if (
|
|
253
|
-
|
|
246
|
+
if (expectedYearStart > now) {
|
|
247
|
+
expectedYearStart.setFullYear(now.getFullYear() - 1);
|
|
254
248
|
}
|
|
255
|
-
await this.adapter.setStateAsync(statePath, calculator.formatDateString(yearStartDate), true);
|
|
256
|
-
} else if (typeof state?.val === 'number') {
|
|
257
|
-
await this.adapter.setStateAsync(statePath, calculator.formatDateString(new Date(state.val)), true);
|
|
258
|
-
} else {
|
|
259
|
-
await this.adapter.setStateAsync(statePath, nowIso, true);
|
|
260
249
|
}
|
|
250
|
+
|
|
251
|
+
if (!expectedYearStart) {
|
|
252
|
+
expectedYearStart = new Date(new Date().getFullYear(), 0, 1, 12, 0, 0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Always set to expected value (fixes mismatches from config changes)
|
|
256
|
+
await this.adapter.setStateAsync(statePath, expectedYearStart.getTime(), true);
|
|
257
|
+
} else {
|
|
258
|
+
// For lastDayStart and lastMonthStart
|
|
259
|
+
if (!state || !state.val || typeof state.val === 'string') {
|
|
260
|
+
// Initialize with current timestamp (convert string to number if needed)
|
|
261
|
+
await this.adapter.setStateAsync(statePath, Date.now(), true);
|
|
262
|
+
}
|
|
263
|
+
// If already a valid number, no action needed (already correct in state)
|
|
261
264
|
}
|
|
262
265
|
}
|
|
263
266
|
|
|
@@ -553,8 +556,9 @@ class MultiMeterManager {
|
|
|
553
556
|
let annualFeeAccumulated = 0;
|
|
554
557
|
|
|
555
558
|
if (yearStartState && yearStartState.val) {
|
|
556
|
-
|
|
557
|
-
|
|
559
|
+
// yearStartState.val is a timestamp (number)
|
|
560
|
+
const yearStartDate = new Date(yearStartState.val);
|
|
561
|
+
if (!isNaN(yearStartDate.getTime())) {
|
|
558
562
|
const now = new Date();
|
|
559
563
|
const yearStartTime = yearStartDate.getTime();
|
|
560
564
|
const nowTime = now.getTime();
|
|
@@ -572,8 +576,9 @@ class MultiMeterManager {
|
|
|
572
576
|
|
|
573
577
|
// Calculate balance and total yearly costs
|
|
574
578
|
if (yearStartState && yearStartState.val) {
|
|
575
|
-
|
|
576
|
-
|
|
579
|
+
// yearStartState.val is a timestamp (number)
|
|
580
|
+
const yearStartDate = new Date(yearStartState.val);
|
|
581
|
+
if (!isNaN(yearStartDate.getTime())) {
|
|
577
582
|
const now = new Date();
|
|
578
583
|
// Calculate paid total based on started months (not just completed months)
|
|
579
584
|
// If current month has started, count it as paid
|
package/lib/stateManager.js
CHANGED
|
@@ -718,6 +718,37 @@ async function createUtilityStateStructure(adapter, type, _config = {}) {
|
|
|
718
718
|
native: {},
|
|
719
719
|
});
|
|
720
720
|
|
|
721
|
+
// HT/NT lastDay states - only create if HT/NT tariff is enabled
|
|
722
|
+
if (_config[htNtEnabledKey]) {
|
|
723
|
+
await adapter.setObjectNotExistsAsync(`${type}.statistics.lastDayHT`, {
|
|
724
|
+
type: 'state',
|
|
725
|
+
common: {
|
|
726
|
+
name: `Verbrauch gestern HT (${label.unit})`,
|
|
727
|
+
type: 'number',
|
|
728
|
+
role: STATE_ROLES.consumption,
|
|
729
|
+
read: true,
|
|
730
|
+
write: false,
|
|
731
|
+
unit: label.unit,
|
|
732
|
+
def: 0,
|
|
733
|
+
},
|
|
734
|
+
native: {},
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
await adapter.setObjectNotExistsAsync(`${type}.statistics.lastDayNT`, {
|
|
738
|
+
type: 'state',
|
|
739
|
+
common: {
|
|
740
|
+
name: `Verbrauch gestern NT (${label.unit})`,
|
|
741
|
+
type: 'number',
|
|
742
|
+
role: STATE_ROLES.consumption,
|
|
743
|
+
read: true,
|
|
744
|
+
write: false,
|
|
745
|
+
unit: label.unit,
|
|
746
|
+
def: 0,
|
|
747
|
+
},
|
|
748
|
+
native: {},
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
721
752
|
if (type === 'gas') {
|
|
722
753
|
await adapter.setObjectNotExistsAsync(`${type}.statistics.lastDayVolume`, {
|
|
723
754
|
type: 'state',
|
package/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* ioBroker
|
|
4
|
+
* ioBroker Utility Monitor Adapter
|
|
5
5
|
* Monitors gas, water, and electricity consumption with cost calculation
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -11,14 +11,14 @@ const BillingManager = require('./lib/billingManager');
|
|
|
11
11
|
const MessagingHandler = require('./lib/messagingHandler');
|
|
12
12
|
const MultiMeterManager = require('./lib/multiMeterManager');
|
|
13
13
|
|
|
14
|
-
class
|
|
14
|
+
class UtilityMonitor extends utils.Adapter {
|
|
15
15
|
/**
|
|
16
16
|
* @param {Partial<utils.AdapterOptions>} [options] - Adapter options
|
|
17
17
|
*/
|
|
18
18
|
constructor(options) {
|
|
19
19
|
super({
|
|
20
20
|
...options,
|
|
21
|
-
name: '
|
|
21
|
+
name: 'utility-monitor',
|
|
22
22
|
});
|
|
23
23
|
this.on('ready', this.onReady.bind(this));
|
|
24
24
|
this.on('stateChange', this.onStateChange.bind(this));
|
|
@@ -290,8 +290,8 @@ if (require.main !== module) {
|
|
|
290
290
|
/**
|
|
291
291
|
* @param {Partial<utils.AdapterOptions>} [options] - Adapter options
|
|
292
292
|
*/
|
|
293
|
-
module.exports = options => new
|
|
293
|
+
module.exports = options => new UtilityMonitor(options);
|
|
294
294
|
} else {
|
|
295
295
|
// otherwise start the instance directly
|
|
296
|
-
new
|
|
296
|
+
new UtilityMonitor();
|
|
297
297
|
}
|