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 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
@@ -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": "Nebenkosten-Monitor",
1498
+ "text": "Utility Monitor",
1465
1499
  "size": 3
1466
1500
  },
1467
1501
  "_infoVersion": {
1468
1502
  "type": "staticText",
1469
- "text": "**Version:** 1.4.0\n\n**Autor:** fischi87",
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.nebenkosten-monitor)",
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,
@@ -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.2",
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": {
@@ -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?.vertragsbeginn;
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?.vertragsbeginn;
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 <= lastValue) {
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;
@@ -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) {
@@ -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 (!state || !state.val || typeof state.val === 'number') {
232
- if (role === 'lastYearStart' && (!state || !state.val)) {
233
- const contractStart = calculator.parseGermanDate(config.contractStart);
234
- let yearStartDate;
235
-
236
- if (contractStart && !isNaN(contractStart.getTime())) {
237
- const now = new Date();
238
- yearStartDate = new Date(
239
- now.getFullYear(),
240
- contractStart.getMonth(),
241
- contractStart.getDate(),
242
- 12,
243
- 0,
244
- 0,
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 (!yearStartDate) {
253
- yearStartDate = new Date(new Date().getFullYear(), 0, 1, 12, 0, 0);
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
- const yearStartDate = calculator.parseDateString(yearStartState.val);
557
- if (yearStartDate && !isNaN(yearStartDate.getTime())) {
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
- const yearStartDate = calculator.parseDateString(yearStartState.val);
576
- if (yearStartDate && !isNaN(yearStartDate.getTime())) {
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
@@ -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 Nebenkosten-Monitor Adapter
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 NebenkostenMonitor extends utils.Adapter {
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: 'nebenkosten-monitor',
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 NebenkostenMonitor(options);
293
+ module.exports = options => new UtilityMonitor(options);
294
294
  } else {
295
295
  // otherwise start the instance directly
296
- new NebenkostenMonitor();
296
+ new UtilityMonitor();
297
297
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.utility-monitor",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Monitor gas, water, and electricity consumption with cost calculation",
5
5
  "author": {
6
6
  "name": "fischi87",