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 +23 -63
- package/admin/jsonConfig.json +3 -37
- package/io-package.json +13 -52
- package/lib/billingManager.js +10 -4
- package/lib/messagingHandler.js +0 -62
- package/lib/multiMeterManager.js +34 -60
- package/main.js +1 -15
- package/package.json +13 -13
- package/admin/tab_m.html +0 -305
- package/lib/importManager.js +0 -344
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 |
|
|
100
|
-
| `basicCharge` | **Grundgebühr akkumuliert**
|
|
101
|
-
| `annualFee` | **Jahresgebühr
|
|
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 | **+
|
|
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:
|
|
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: +
|
|
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
|
-
###
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
- **
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
- **
|
|
244
|
-
|
|
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
|
package/admin/jsonConfig.json
CHANGED
|
@@ -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": "
|
|
1464
|
+
"text": "Nebenkosten-Monitor",
|
|
1499
1465
|
"size": 3
|
|
1500
1466
|
},
|
|
1501
1467
|
"_infoVersion": {
|
|
1502
1468
|
"type": "staticText",
|
|
1503
|
-
"text": "**Version:** 1.4.
|
|
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.
|
|
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
|
+
"version": "1.4.5",
|
|
5
5
|
"news": {
|
|
6
|
-
"1.4.
|
|
7
|
-
"en": "Fix:
|
|
8
|
-
"de": "Fix:
|
|
9
|
-
"ru": "Исправление:
|
|
10
|
-
"pt": "Correção:
|
|
11
|
-
"nl": "Fix:
|
|
12
|
-
"fr": "Correction:
|
|
13
|
-
"it": "Correzione:
|
|
14
|
-
"es": "Corrección:
|
|
15
|
-
"pl": "Naprawa:
|
|
16
|
-
"uk": "Виправлення:
|
|
17
|
-
"zh-cn": "
|
|
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": {
|
package/lib/billingManager.js
CHANGED
|
@@ -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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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(
|
|
197
|
+
calculator.roundToDecimals(basicChargeAccumulated, 2),
|
|
192
198
|
true,
|
|
193
199
|
);
|
|
194
200
|
|
package/lib/messagingHandler.js
CHANGED
|
@@ -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) {
|
package/lib/multiMeterManager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
578
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
591
|
+
await this.adapter.setStateAsync(
|
|
592
|
+
`${basePath}.costs.totalYearly`,
|
|
593
|
+
calculator.roundToDecimals(totalYearlyCost, 2),
|
|
594
|
+
true,
|
|
595
|
+
);
|
|
596
596
|
|
|
597
|
-
|
|
598
|
-
|
|
597
|
+
const paidTotal = (config.abschlag || 0) * monthsSinceYearStart;
|
|
598
|
+
const balance = totalYearlyCost - paidTotal;
|
|
599
599
|
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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.
|
|
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": "
|
|
43
|
-
"@alcalzone/release-script-plugin-iobroker": "
|
|
44
|
-
"@alcalzone/release-script-plugin-license": "
|
|
45
|
-
"@alcalzone/release-script-plugin-manual-review": "
|
|
46
|
-
"@iobroker/adapter-dev": "
|
|
47
|
-
"@iobroker/dev-server": "
|
|
48
|
-
"@iobroker/eslint-config": "
|
|
49
|
-
"@iobroker/testing": "
|
|
50
|
-
"@tsconfig/node20": "
|
|
51
|
-
"@types/iobroker": "npm:@iobroker/types
|
|
52
|
-
"@types/node": "
|
|
53
|
-
"typescript": "
|
|
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>
|
package/lib/importManager.js
DELETED
|
@@ -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;
|