iobroker.utility-monitor 1.4.4 → 1.4.5

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
@@ -96,11 +96,11 @@ Für jede aktivierte Verbrauchsart (Gas/Wasser/Strom/PV) werden folgende Ordner
96
96
  | `daily` | Kosten **heute** | daily × Arbeitspreis | 2,27 € |
97
97
  | `monthly` | Kosten **diesen Monat** | monthly × Arbeitspreis | 21,61 € |
98
98
  | `yearly` | **Verbrauchskosten** seit Vertragsbeginn | yearly × Arbeitspreis | 137,61 € |
99
- | `totalYearly` | **Gesamtkosten Jahr** (Verbrauch + alle Fixkosten) | yearly-cost + basicCharge + annualFee | 162,64 € |
100
- | `basicCharge` | **Grundgebühr akkumuliert** (inkl. Jahresgebühr anteilig) | (Grundgebühr + (Jahresgebühr/12)) × Monate | 19,20 € |
101
- | `annualFee` | **Jahresgebühr akkumuliert** | (Jahresgebühr / 12) × Monate | 4,17 |
99
+ | `totalYearly` | **Gesamtkosten Jahr** (Verbrauch + alle Fixkosten) | yearly-cost + basicCharge + annualFee | 212,64 € |
100
+ | `basicCharge` | **Grundgebühr akkumuliert** | Grundgebühr × Monate | 15,03 € |
101
+ | `annualFee` | **Jahresgebühr** (fester Wert pro Jahr) | Jahresgebühr (aus Config) | 60,00 |
102
102
  | `paidTotal` | **Bezahlt** via Abschlag | Abschlag × Monate | 150,00 € |
103
- | `balance` | **🎯 WICHTIGSTER Wert!**<br>Nachzahlung (+) oder Guthaben (-) | totalYearly - paidTotal | **+12,64 €**<br>→ Nachzahlung! |
103
+ | `balance` | **🎯 WICHTIGSTER Wert!**<br>Nachzahlung (+) oder Guthaben (-) | totalYearly - paidTotal | **+62,64 €**<br>→ Nachzahlung! |
104
104
 
105
105
  #### 🔍 **balance** genauer erklärt:
106
106
 
@@ -112,13 +112,14 @@ Für jede aktivierte Verbrauchsart (Gas/Wasser/Strom/PV) werden folgende Ordner
112
112
 
113
113
  ```
114
114
  Verbrauchskosten: 137,61 € (yearly)
115
- Grundgebühr: + 15,03 € (basicCharge)
115
+ Grundgebühr: + 15,03 € (basicCharge - 1 Monat × 15,03€)
116
+ Jahresgebühr: + 60,00 € (annualFee - fester Wert)
116
117
  ────────────────────────────
117
- Gesamtkosten: 152,64 €
118
+ Gesamtkosten: 212,64 € (totalYearly)
118
119
 
119
- Bezahlt (Abschlag): 150,00 € (paidTotal)
120
+ Bezahlt (Abschlag): 150,00 € (paidTotal - 1 Monat × 150€)
120
121
  ────────────────────────────
121
- Balance: +2,64 € → Nachzahlung
122
+ Balance: +62,64 € → Nachzahlung
122
123
  ```
123
124
 
124
125
  ---
@@ -228,23 +229,20 @@ Der Adapter setzt Zähler automatisch zurück:
228
229
 
229
230
  ## Changelog
230
231
 
231
- ### **WORK IN PROGRESS**
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
232
+ ### 1.4.5 (2026-01-20)
233
+
234
+ - **FIX:** 🐛 **Critical Multi-Meter Cost Calculation Bugs** - Comprehensive fixes for multi-meter functionality:
235
+ - **Main Meter Sync Issue**: Removed duplicate initialization that prevented `lastSync` updates on main meter
236
+ - **basicCharge Accumulation**: Now correctly calculates `basicCharge = grundgebuehr × months` (was showing only 1 month)
237
+ - **paidTotal Accumulation**: Now correctly calculates `paidTotal = abschlag × months` (was showing only 1 month)
238
+ - **annualFee as Fixed Value**: Jahresgebühr is now used as fixed yearly value (e.g., 60€ stays 60€)
239
+ - Previously incorrectly treated as monthly fee and multiplied by months
240
+ - User-entered value in config (e.g., 60€) is now used directly as intended
241
+ - **Balance Formula Corrected**: Fixed reversed formula `balance = totalYearly - paidTotal`
242
+ - Positive balance = Nachzahlung (you owe money)
243
+ - Negative balance = Guthaben (you get money back)
244
+ - **IMPROVED:** 📦 **Dev-Dependencies**: Updated from tilde (~) to caret (^) versioning for better security updates
245
+ - **CLEANUP:** 🧹 **Repository Compliance**: Removed unpublished versions from changelog (resolves ioBroker Bot Issue #1)
248
246
 
249
247
  ### 1.4.2 (2026-01-18)
250
248
 
@@ -268,44 +266,6 @@ Der Adapter setzt Zähler automatisch zurück:
268
266
  - Prevents silent failures in StateManager
269
267
  - **IMPROVED:** 🧪 **Code Quality** - All tests passing (31 unit + 57 package tests)
270
268
 
271
- ### 1.4.1 (2026-01-18)
272
-
273
- - **FIX:** 🐛 **Multi-Meter Critical Bugs** - Comprehensive fixes for multi-meter functionality:
274
- - Fixed `updateCosts()` to correctly delegate to multiMeterManager for all meters
275
- - Fixed `closeBillingPeriod()` to archive totals instead of only main meter values
276
- - Fixed `checkMonthlyReport()` to display totals in reports for multi-meter setups
277
- - Fixed state type mismatch: `lastDayStart`, `lastMonthStart`, `lastYearStart` now use number (timestamp) instead of string
278
- - **NEW:** 🎯 **Per-Meter Billing Closure** - Each meter can now be closed individually with its own `billing.closePeriod` button
279
- - Main meter: `gas.billing.closePeriod`
280
- - Additional meters: `gas.erdgeschoss.billing.closePeriod`, `gas.keller.billing.closePeriod`, etc.
281
- - Each meter uses its own contract date for yearly resets
282
- - **NEW:** 📅 **Individual Contract Anniversary Resets** - Each meter resets on its own contract date
283
- - Primary: Manual `closePeriod` triggers yearly reset immediately
284
- - Fallback: Automatic reset on contract anniversary if user forgets to close period
285
- - Contract date is preserved when closing period early (no drift)
286
- - **IMPROVED:** 💰 **Billing Period Closure** - No longer resets `basicCharge` and `annualFee` to zero
287
- - These values now persist from config (user must update config if tariff changes)
288
- - Helpful reminder message added after closing period
289
- - **FIX:** 🤖 **ioBroker Bot Compliance** - All bot checker issues resolved:
290
- - Removed non-existent version 1.3.4 from news
291
- - Added complete translations for all news entries (9 languages)
292
- - Removed `.npmignore` file (using `files` field in package.json)
293
- - DevDependencies already use `~` syntax (compliant)
294
-
295
- ### 1.4.0 (2026-01-17)
296
-
297
- - **NEW:** 🎉 **Multi-Meter Support** - Verwende mehrere Zähler pro Typ (z.B. Gas Hauptzähler + Werkstatt-Zähler)
298
- - Beliebig viele zusätzliche Zähler mit eigenen Namen konfigurierbar
299
- - Separate Kostenberechnung und Statistiken pro Zähler
300
- - Automatische Totals-Berechnung über alle Zähler
301
- - **NEW:** ✨ **Komma-Dezimaltrenner Support** - Admin UI akzeptiert jetzt sowohl Komma als auch Punkt (z.B. `12,50` oder `12.50`)
302
- - **NEW:** 📊 **Pro-Meter Billing** - Jeder Zähler hat eigene `billing.daysRemaining` und `billing.periodEnd` Werte
303
- - **NEW:** 🔧 **Config-Parser** - Automatische Konvertierung von String→Number mit Komma-Support
304
- - **FIX:** 💰 **Balance-Berechnung korrigiert** - Nutzt jetzt begonnene Monate statt volle Monate (17 Tage = 1 Monat gezahlt)
305
- - **FIX:** 🐛 **String-Type Fehler** behoben - Config-Werte werden korrekt als Numbers verarbeitet
306
- - **IMPROVED:** 🔍 **Debug-Logging** - Hilfreiche Debug-Logs für Troubleshooting (nur in Debug-Modus sichtbar)
307
- - **CLEANUP:** 🧹 Repository aufgeräumt - Alte Backup-Dateien und temporäre Scripts entfernt
308
-
309
269
  ---
310
270
 
311
271
  ## License
@@ -1455,52 +1455,18 @@
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
- },
1492
1458
  "tabInfo": {
1493
1459
  "type": "panel",
1494
1460
  "label": "ℹ️ Info & Hilfe",
1495
1461
  "items": {
1496
1462
  "_infoHeader": {
1497
1463
  "type": "header",
1498
- "text": "Utility Monitor",
1464
+ "text": "Nebenkosten-Monitor",
1499
1465
  "size": 3
1500
1466
  },
1501
1467
  "_infoVersion": {
1502
1468
  "type": "staticText",
1503
- "text": "**Version:** 1.4.4\n\n**Autor:** fischi87",
1469
+ "text": "**Version:** 1.4.0\n\n**Autor:** fischi87",
1504
1470
  "sm": 12,
1505
1471
  "xs": 12,
1506
1472
  "md": 12,
@@ -1509,7 +1475,7 @@
1509
1475
  },
1510
1476
  "donation": {
1511
1477
  "type": "staticText",
1512
- "text": "☕ [PayPal Spenden](https://paypal.me/bigplay87) | 📁 [GitHub Repository](https://github.com/fischi87/ioBroker.utility-monitor)",
1478
+ "text": "☕ [PayPal Spenden](https://paypal.me/bigplay87) | 📁 [GitHub Repository](https://github.com/fischi87/ioBroker.nebenkosten-monitor)",
1513
1479
  "xs": 12,
1514
1480
  "sm": 12,
1515
1481
  "md": 12,
package/io-package.json CHANGED
@@ -1,33 +1,20 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "utility-monitor",
4
- "version": "1.4.4",
4
+ "version": "1.4.5",
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计算错误 - 现在在传感器更新后正确计算(月付款×月数)"
6
+ "1.4.5": {
7
+ "en": "Fix: Critical multi-meter cost calculation bugs (main meter sync, basicCharge/paidTotal accumulation, annualFee as fixed yearly value). Fix: Balance formula corrected. Fix: Removed duplicate initialization causing sync issues.",
8
+ "de": "Fix: Kritische Multi-Meter Kostenberechnungsfehler (Hauptzähler-Sync, basicCharge/paidTotal Akkumulation, Jahresgebühr als fester Jahreswert). Fix: Balance-Formel korrigiert. Fix: Doppelte Initialisierung entfernt.",
9
+ "ru": "Исправление: Критические ошибки расчета затрат multi-meter (синхронизация главного счетчика, накопление basicCharge/paidTotal, annualFee как фиксированное годовое значение). Исправление: Формула баланса исправлена. Исправление: Удалена дублирующая инициализация.",
10
+ "pt": "Correção: Bugs críticos de cálculo de custos multi-medidor (sincronização medidor principal, acumulação basicCharge/paidTotal, annualFee como valor anual fixo). Correção: Fórmula de saldo corrigida. Correção: Inicialização duplicada removida.",
11
+ "nl": "Fix: Kritieke multi-meter kostenberekeningsfouten (hoofdmeter sync, basicCharge/paidTotal accumulatie, annualFee als vaste jaarwaarde). Fix: Balans formule gecorrigeerd. Fix: Dubbele initialisatie verwijderd.",
12
+ "fr": "Correction: Bugs critiques calcul coûts multi-compteurs (sync compteur principal, accumulation basicCharge/paidTotal, annualFee comme valeur annuelle fixe). Correction: Formule de solde corrigée. Correction: Initialisation en double supprimée.",
13
+ "it": "Correzione: Bug critici calcolo costi multi-contatore (sync contatore principale, accumulazione basicCharge/paidTotal, annualFee come valore annuale fisso). Correzione: Formula bilancio corretta. Correzione: Inizializzazione duplicata rimossa.",
14
+ "es": "Corrección: Bugs críticos cálculo costos multi-medidor (sincronización medidor principal, acumulación basicCharge/paidTotal, annualFee como valor anual fijo). Corrección: Fórmula balance corregida. Corrección: Inicialización duplicada eliminada.",
15
+ "pl": "Naprawa: Krytyczne błędy obliczania kosztów multi-meter (synchronizacja głównego licznika, akumulacja basicCharge/paidTotal, annualFee jako stała wartość roczna). Naprawa: Formuła salda poprawiona. Naprawa: Usunięto podwójną inicjalizację.",
16
+ "uk": "Виправлення: Критичні помилки розрахунку витрат multi-meter (синхронізація головного лічильника, накопичення basicCharge/paidTotal, annualFee як фіксоване річне значення). Виправлення: Формула балансу виправлена. Виправлення: Видалено подвійну ініціалізацію.",
17
+ "zh-cn": "修复:关键多表成本计算错误(主表同步、basicCharge/paidTotal累积、annualFee作为固定年值)。修复:余额公式已更正。修复:删除重复初始化。"
31
18
  },
32
19
  "1.4.2": {
33
20
  "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.",
@@ -41,32 +28,6 @@
41
28
  "pl": "Naprawa: Krytyczny błąd salda multi-meter (zakodowane 12 miesięcy). Naprawa: Naprawione błędy TypeScript. Nowe: Ulepszona walidacja danych. Nowe: Rozszerzone stałe. Nowe: Wrapper obsługi błędów.",
42
29
  "uk": "Виправлення: Критична помилка балансу multi-meter (жорстко задані 12 місяців). Виправлення: Усунено помилки TypeScript. Нове: Покращена перевірка даних. Нове: Розширені константи. Нове: Обробник помилок.",
43
30
  "zh-cn": "修复:关键多表余额错误(硬编码12个月)。修复:解决TypeScript错误。新增:增强输入验证。新增:扩展常量。新增:错误处理包装器。"
44
- },
45
- "1.4.1": {
46
- "en": "Fix: Multi-meter bugs (cost calculation, billing period closure, monthly reports, yearly resets). Fix: Per-meter billing closure support. Fix: State type corrections (lastDayStart). Fix: ioBroker Bot compliance.",
47
- "de": "Fix: Multi-Meter-Fehler (Kostenberechnung, Periodenabschluss, Monatsberichte, Jahres-Resets). Fix: Zähler-individuelle Periodenabschlüsse. Fix: State-Typ-Korrekturen (lastDayStart). Fix: ioBroker Bot-Compliance.",
48
- "ru": "Исправление: Ошибки нескольких счетчиков (расчет затрат, закрытие периода, ежемесячные отчеты, годовые сбросы). Исправление: Поддержка закрытия периода для каждого счетчика. Исправление: Исправления типов состояний. Исправление: Соответствие ioBroker Bot.",
49
- "pt": "Correção: Bugs multi-medidor (cálculo custos, fechamento período, relatórios mensais, resets anuais). Correção: Suporte fechamento período por medidor. Correção: Correções tipo estado. Correção: Conformidade Bot ioBroker.",
50
- "nl": "Fix: Multi-meter bugs (kostenberekening, periode afsluiting, maandraporten, jaarlijkse resets). Fix: Per-meter periode afsluiting. Fix: State type correcties. Fix: ioBroker Bot compliance.",
51
- "fr": "Correction: Bugs multi-compteurs (calcul coûts, clôture période, rapports mensuels, réinitialisations annuelles). Correction: Support clôture période par compteur. Correction: Corrections type d'état. Correction: Conformité Bot ioBroker.",
52
- "it": "Correzione: Bug multi-contatore (calcolo costi, chiusura periodo, report mensili, reset annuali). Correzione: Supporto chiusura periodo per contatore. Correzione: Correzioni tipo stato. Correzione: Conformità Bot ioBroker.",
53
- "es": "Corrección: Bugs multi-medidor (cálculo costos, cierre período, informes mensuales, reinicios anuales). Corrección: Soporte cierre período por medidor. Corrección: Correcciones tipo estado. Corrección: Cumplimiento Bot ioBroker.",
54
- "pl": "Naprawa: Błędy wielu liczników (obliczanie kosztów, zamknięcie okresu, raporty miesięczne, resety roczne). Naprawa: Wsparcie zamknięcia okresu dla licznika. Naprawa: Poprawki typu stanu. Naprawa: Zgodność z Bot ioBroker.",
55
- "uk": "Виправлення: Помилки кількох лічильників (розрахунок витрат, закриття періоду, місячні звіти, річні скидання). Виправлення: Підтримка закриття періоду для лічильника. Виправлення: Виправлення типу стану. Виправлення: Відповідність ioBroker Bot.",
56
- "zh-cn": "修复:多表错误(成本计算、期间关闭、月度报告、年度重置)。修复:每表期间关闭支持。修复:状态类型更正。修复:ioBroker Bot合规性。"
57
- },
58
- "1.4.0": {
59
- "en": "New: Multi-Meter Support! Add unlimited custom-named meters per utility type (gas, water, electricity). Automatic totals calculation. Fully backward compatible.",
60
- "de": "Neu: Multi-Meter-Unterstützung! Unbegrenzt viele Zähler pro Medium (Gas, Wasser, Strom) mit benutzerdefinierten Namen. Automatische Gesamt-Summen. Vollständig rückwärtskompatibel.",
61
- "ru": "Новое: Поддержка нескольких счетчиков! Добавьте неограниченное количество пользовательских счетчиков для каждого типа коммунальных услуг (газ, вода, электричество). Автоматический расчет итогов. Полная обратная совместимость.",
62
- "pt": "Novo: Suporte Multi-Medidor! Adicione medidores personalizados ilimitados por tipo de utilidade (gás, água, eletricidade). Cálculo automático de totais. Totalmente compatível com versões anteriores.",
63
- "nl": "Nieuw: Multi-Meter Ondersteuning! Voeg onbeperkt aangepaste meters toe per type (gas, water, elektriciteit). Automatische totaalberekening. Volledig achterwaarts compatibel.",
64
- "fr": "Nouveau: Support Multi-Compteur! Ajoutez des compteurs personnalisés illimités par type de service (gaz, eau, électricité). Calcul automatique des totaux. Entièrement rétrocompatible.",
65
- "it": "Nuovo: Supporto Multi-Contatore! Aggiungi contatori personalizzati illimitati per tipo di servizio (gas, acqua, elettricità). Calcolo automatico dei totali. Completamente retrocompatibile.",
66
- "es": "Nuevo: Soporte Multi-Medidor! Agregue medidores personalizados ilimitados por tipo de servicio (gas, agua, electricidad). Cálculo automático de totales. Totalmente compatible con versiones anteriores.",
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.",
68
- "uk": "Нове: Підтримка кількох лічильників! Додайте необмежену кількість власних лічильників для кожного типу комунальних послуг (газ, вода, електрика). Автоматичний розрахунок підсумків. Повна зворотна сумісність.",
69
- "zh-cn": "新增:多表支持!为每种公用事业类型(燃气、水、电)添加无限的自定义计量表。自动总计计算。完全向后兼容。"
70
31
  }
71
32
  },
72
33
  "titleLang": {
@@ -155,10 +155,14 @@ class BillingManager {
155
155
  monthsSinceContract = Math.max(1, yDiff * 12 + mDiff + 1);
156
156
  }
157
157
 
158
+ // Calculate accumulated basic charge (monthly fee × months)
158
159
  const basicChargeAccumulated = basicChargeMonthly * monthsSinceContract;
159
- const annualFeeAccumulated = (annualFeePerYear / 12) * monthsSinceContract;
160
- const totalFixCostsAccumulated = basicChargeAccumulated + annualFeeAccumulated;
161
- const totalYearlyCost = yearlyConsumptionCost + totalFixCostsAccumulated;
160
+
161
+ // Jahresgebühr ist ein FIXER Wert pro Jahr (z.B. 60€)
162
+ // NICHT pro-rata nach Monaten/Tagen berechnen!
163
+ const annualFeeAccumulated = annualFeePerYear;
164
+
165
+ const totalYearlyCost = yearlyConsumptionCost + basicChargeAccumulated + annualFeeAccumulated;
162
166
 
163
167
  // Update states
164
168
  await this.adapter.setStateAsync(
@@ -186,9 +190,11 @@ class BillingManager {
186
190
  calculator.roundToDecimals(annualFeeAccumulated, 2),
187
191
  true,
188
192
  );
193
+ // basicCharge enthält NUR die monatliche Grundgebühr (akkumuliert)
194
+ // Jahresgebühr ist separat in annualFee
189
195
  await this.adapter.setStateAsync(
190
196
  `${type}.costs.basicCharge`,
191
- calculator.roundToDecimals(totalFixCostsAccumulated, 2),
197
+ calculator.roundToDecimals(basicChargeAccumulated, 2),
192
198
  true,
193
199
  );
194
200
 
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const ImportManager = require('./importManager');
4
-
5
3
  /**
6
4
  * MessagingHandler handles all incoming adapter messages
7
5
  * and outgoing notifications.
@@ -12,7 +10,6 @@ class MessagingHandler {
12
10
  */
13
11
  constructor(adapter) {
14
12
  this.adapter = adapter;
15
- this.importManager = null; // Lazy initialization
16
13
  }
17
14
 
18
15
  /**
@@ -139,65 +136,6 @@ class MessagingHandler {
139
136
  );
140
137
  }
141
138
  }
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
- }
201
139
  } else {
202
140
  this.adapter.log.warn(`[onMessage] Unknown command: ${obj.command}`);
203
141
  if (obj.callback) {
@@ -549,86 +549,60 @@ class MultiMeterManager {
549
549
  await this.adapter.setStateAsync(`${basePath}.costs.monthly`, calculator.roundToDecimals(monthlyCost, 2), true);
550
550
  await this.adapter.setStateAsync(`${basePath}.costs.yearly`, calculator.roundToDecimals(yearlyCost, 2), true);
551
551
 
552
- await this.adapter.setStateAsync(`${basePath}.costs.basicCharge`, Number(config.grundgebuehr) || 0, true);
553
-
554
- // Calculate annual fee (prorated)
552
+ // Calculate accumulated costs based on contract start
555
553
  const yearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
556
- let annualFeeAccumulated = 0;
554
+ let monthsSinceYearStart = 1;
555
+ let basicChargeAccumulated = 0;
557
556
 
558
557
  if (yearStartState && yearStartState.val) {
559
558
  // yearStartState.val is a timestamp (number)
560
559
  const yearStartDate = new Date(yearStartState.val);
561
560
  if (!isNaN(yearStartDate.getTime())) {
562
561
  const now = new Date();
563
- const yearStartTime = yearStartDate.getTime();
564
- const nowTime = now.getTime();
565
- const daysSinceYearStart = Math.floor((nowTime - yearStartTime) / (1000 * 60 * 60 * 24));
566
- const daysInYear = calculator.isLeapYear(now.getFullYear()) ? 366 : 365;
567
- annualFeeAccumulated = ((config.jahresgebuehr || 0) / daysInYear) * daysSinceYearStart;
562
+
563
+ // Calculate months since contract start (started months, including current)
564
+ monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, now) + 1;
565
+
566
+ // Calculate accumulated basic charge (monthly fee × months)
567
+ basicChargeAccumulated = (config.grundgebuehr || 0) * monthsSinceYearStart;
568
568
  }
569
569
  }
570
570
 
571
+ // Jahresgebühr ist ein FIXER Wert pro Jahr (z.B. 60€)
572
+ // NICHT pro-rata nach Monaten/Tagen berechnen!
573
+ const annualFeeAccumulated = config.jahresgebuehr || 0;
574
+
575
+ // Update basicCharge with accumulated value (not just monthly!)
576
+ await this.adapter.setStateAsync(
577
+ `${basePath}.costs.basicCharge`,
578
+ calculator.roundToDecimals(basicChargeAccumulated, 2),
579
+ true,
580
+ );
581
+
571
582
  await this.adapter.setStateAsync(
572
583
  `${basePath}.costs.annualFee`,
573
584
  calculator.roundToDecimals(annualFeeAccumulated, 2),
574
585
  true,
575
586
  );
576
587
 
577
- // Calculate balance and total yearly costs
578
- if (yearStartState && yearStartState.val) {
579
- // yearStartState.val is a timestamp (number)
580
- const yearStartDate = new Date(yearStartState.val);
581
- if (!isNaN(yearStartDate.getTime())) {
582
- const now = new Date();
583
- // Calculate paid total based on started months (not just completed months)
584
- // If current month has started, count it as paid
585
- const monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, now) + 1;
586
-
587
- // Calculate total yearly costs with correct months
588
- const basicChargeAccumulated = (config.grundgebuehr || 0) * monthsSinceYearStart;
589
- const totalYearlyCost = yearlyCost + basicChargeAccumulated + annualFeeAccumulated;
588
+ // Calculate total yearly costs and balance
589
+ const totalYearlyCost = yearlyCost + basicChargeAccumulated + annualFeeAccumulated;
590
590
 
591
- await this.adapter.setStateAsync(
592
- `${basePath}.costs.totalYearly`,
593
- calculator.roundToDecimals(totalYearlyCost, 2),
594
- true,
595
- );
591
+ await this.adapter.setStateAsync(
592
+ `${basePath}.costs.totalYearly`,
593
+ calculator.roundToDecimals(totalYearlyCost, 2),
594
+ true,
595
+ );
596
596
 
597
- const paidTotal = (config.abschlag || 0) * monthsSinceYearStart;
598
- const balance = paidTotal - totalYearlyCost;
597
+ const paidTotal = (config.abschlag || 0) * monthsSinceYearStart;
598
+ const balance = totalYearlyCost - paidTotal;
599
599
 
600
- this.adapter.log.debug(
601
- `[${basePath}] Balance calculation: abschlag=${config.abschlag}, months=${monthsSinceYearStart}, paidTotal=${paidTotal.toFixed(2)}, totalYearly=${totalYearlyCost.toFixed(2)}, balance=${balance.toFixed(2)}`,
602
- );
600
+ this.adapter.log.debug(
601
+ `[${basePath}] Balance calculation: grundgebuehr=${config.grundgebuehr}, jahresgebuehr=${config.jahresgebuehr}, abschlag=${config.abschlag}, months=${monthsSinceYearStart}, basicCharge=${basicChargeAccumulated.toFixed(2)}, annualFee=${annualFeeAccumulated.toFixed(2)}, paidTotal=${paidTotal.toFixed(2)}, totalYearly=${totalYearlyCost.toFixed(2)}, balance=${balance.toFixed(2)}`,
602
+ );
603
603
 
604
- await this.adapter.setStateAsync(
605
- `${basePath}.costs.paidTotal`,
606
- calculator.roundToDecimals(paidTotal, 2),
607
- true,
608
- );
609
- await this.adapter.setStateAsync(
610
- `${basePath}.costs.balance`,
611
- calculator.roundToDecimals(balance, 2),
612
- true,
613
- );
614
- } else {
615
- // Fallback if yearStartDate parsing fails
616
- const totalYearlyCost = yearlyCost + annualFeeAccumulated;
617
- await this.adapter.setStateAsync(
618
- `${basePath}.costs.totalYearly`,
619
- calculator.roundToDecimals(totalYearlyCost, 2),
620
- true,
621
- );
622
- }
623
- } else {
624
- // Fallback if no yearStartState exists
625
- const totalYearlyCost = yearlyCost + annualFeeAccumulated;
626
- await this.adapter.setStateAsync(
627
- `${basePath}.costs.totalYearly`,
628
- calculator.roundToDecimals(totalYearlyCost, 2),
629
- true,
630
- );
631
- }
604
+ await this.adapter.setStateAsync(`${basePath}.costs.paidTotal`, calculator.roundToDecimals(paidTotal, 2), true);
605
+ await this.adapter.setStateAsync(`${basePath}.costs.balance`, calculator.roundToDecimals(balance, 2), true);
632
606
  }
633
607
 
634
608
  /**
package/main.js CHANGED
@@ -44,26 +44,12 @@ class UtilityMonitor extends utils.Adapter {
44
44
  this.multiMeterManager = new MultiMeterManager(this, this.consumptionManager, this.billingManager);
45
45
 
46
46
  // Initialize each utility type based on configuration
47
+ // Note: initializeUtility() internally calls multiMeterManager.initializeType()
47
48
  await this.initializeUtility('gas', this.config.gasAktiv);
48
49
  await this.initializeUtility('water', this.config.wasserAktiv);
49
50
  await this.initializeUtility('electricity', this.config.stromAktiv);
50
-
51
51
  await this.initializeUtility('pv', this.config.pvAktiv);
52
52
 
53
- // Initialize Multi-Meter structures for each active type
54
- if (this.config.gasAktiv) {
55
- await this.multiMeterManager.initializeType('gas');
56
- }
57
- if (this.config.wasserAktiv) {
58
- await this.multiMeterManager.initializeType('water');
59
- }
60
- if (this.config.stromAktiv) {
61
- await this.multiMeterManager.initializeType('electricity');
62
- }
63
- if (this.config.pvAktiv) {
64
- await this.multiMeterManager.initializeType('pv');
65
- }
66
-
67
53
  // Initialize General Info States
68
54
  await this.setObjectNotExistsAsync('info', {
69
55
  type: 'channel',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.utility-monitor",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "Monitor gas, water, and electricity consumption with cost calculation",
5
5
  "author": {
6
6
  "name": "fischi87",
@@ -39,18 +39,18 @@
39
39
  "@iobroker/adapter-core": "^3.3.2"
40
40
  },
41
41
  "devDependencies": {
42
- "@alcalzone/release-script": "~5.0.0",
43
- "@alcalzone/release-script-plugin-iobroker": "~4.0.0",
44
- "@alcalzone/release-script-plugin-license": "~4.0.0",
45
- "@alcalzone/release-script-plugin-manual-review": "~4.0.0",
46
- "@iobroker/adapter-dev": "~1.5.0",
47
- "@iobroker/dev-server": "~0.8.0",
48
- "@iobroker/eslint-config": "~2.2.0",
49
- "@iobroker/testing": "~5.2.2",
50
- "@tsconfig/node20": "~20.1.8",
51
- "@types/iobroker": "npm:@iobroker/types@~7.1.0",
52
- "@types/node": "~20.19.27",
53
- "typescript": "~5.9.3"
42
+ "@alcalzone/release-script": "^5.0.0",
43
+ "@alcalzone/release-script-plugin-iobroker": "^4.0.0",
44
+ "@alcalzone/release-script-plugin-license": "^4.0.0",
45
+ "@alcalzone/release-script-plugin-manual-review": "^4.0.0",
46
+ "@iobroker/adapter-dev": "^1.5.0",
47
+ "@iobroker/dev-server": "^0.8.0",
48
+ "@iobroker/eslint-config": "^2.2.0",
49
+ "@iobroker/testing": "^5.2.2",
50
+ "@tsconfig/node20": "^20.1.8",
51
+ "@types/iobroker": "npm:@iobroker/types@^7.1.0",
52
+ "@types/node": "^20.19.27",
53
+ "typescript": "^5.9.3"
54
54
  },
55
55
  "main": "main.js",
56
56
  "files": [
package/admin/tab_m.html DELETED
@@ -1,305 +0,0 @@
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>
@@ -1,344 +0,0 @@
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;