iobroker.nebenkosten-monitor 1.3.2 → 1.3.3
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 +30 -3
- package/admin/jsonConfig.json +102 -1
- package/io-package.json +13 -9
- package/lib/importManager.js +166 -0
- package/lib/importers/abstractImporter.js +35 -0
- package/lib/importers/ehb.js +91 -0
- package/main.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
### ✨ Hauptfunktionen
|
|
16
16
|
|
|
17
|
-
- 📊 **Verbrauchsüberwachung** für Gas, Wasser und
|
|
17
|
+
- 📊 **Verbrauchsüberwachung** für Gas, Wasser, Strom und **PV/Einspeisung**
|
|
18
18
|
- 💰 **Automatische Kostenberechnung** mit Arbeitspreis und Grundgebühr
|
|
19
|
+
- ☀️ **PV & Einspeisung** - Überwache deine Einspeisung und Vergütung
|
|
19
20
|
- 💳 **Abschlagsüberwachung** - Sehe sofort ob Nachzahlung oder Guthaben droht
|
|
20
21
|
- 🔄 **Flexible Sensoren** - Nutzt vorhandene Sensoren (Shelly, Tasmota, Homematic, etc.)
|
|
21
22
|
- ⚡ **HT/NT-Tarife** - Volle Unterstützung für Hoch- und Nebentarife (Tag/Nacht)
|
|
22
|
-
-
|
|
23
|
+
- � **CSV-Import** - Importiere historische Daten (z.B. aus der EhB+ App)
|
|
24
|
+
- �🔄 **Gas-Spezial** - Automatische Umrechnung von m³ in kWh
|
|
23
25
|
- 🕛 **Automatische Resets** - Täglich, monatlich und jährlich (Vertragsjubiläum)
|
|
24
26
|
- 🔔 **Intelligente Benachrichtigungen** - Getrennte Erinnerungen für Abrechnungsende (Zählerstand) und Vertragswechsel (Tarif-Check) mit einstellbaren Vorlaufzeiten.
|
|
25
27
|
|
|
@@ -61,7 +63,7 @@ Gefällt dir dieser Adapter? Du kannst mich gerne mit einem Kaffee unterstützen
|
|
|
61
63
|
|
|
62
64
|
## 📊 Datenpunkte erklärt
|
|
63
65
|
|
|
64
|
-
Für jede aktivierte Verbrauchsart (Gas/Wasser/Strom) werden folgende Ordner angelegt:
|
|
66
|
+
Für jede aktivierte Verbrauchsart (Gas/Wasser/Strom/PV) werden folgende Ordner angelegt:
|
|
65
67
|
|
|
66
68
|
### 🗂️ **consumption** (Verbrauch)
|
|
67
69
|
|
|
@@ -214,6 +216,25 @@ Gasverbrauch wird in **m³ gemessen**, aber in **kWh abgerechnet**.
|
|
|
214
216
|
|
|
215
217
|
---
|
|
216
218
|
|
|
219
|
+
### 📥 CSV Import & Historische Daten
|
|
220
|
+
|
|
221
|
+
Du kannst historische Daten importieren, um deine Jahresstatistik zu vervollständigen.
|
|
222
|
+
|
|
223
|
+
1. Gehe in den Tab **Import**.
|
|
224
|
+
2. Wähle das **Ziel-Medium** (Strom, Gas, Wasser, PV) oder **Benutzerdefiniert**.
|
|
225
|
+
3. Wähle das **Format** (derzeit "EhB+ App (CSV)").
|
|
226
|
+
4. Füge den **CSV-Inhalt** ein.
|
|
227
|
+
- Format: `Datum;Zählerstand;Kommentar` (z.B. `01.01.2023 00:00;12345,6;Start`)
|
|
228
|
+
5. Klicke auf **Importieren**.
|
|
229
|
+
|
|
230
|
+
**Funktionen:**
|
|
231
|
+
|
|
232
|
+
- Berechnet automatisch den Jahresverbrauch für vergangene Jahre.
|
|
233
|
+
- Erstellt die Historie unter `history.JJJJ`.
|
|
234
|
+
- Benutzerdefinierte Zähler ("Einliegerwohnung") werden automatisch angelegt.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
217
238
|
### 🔄 Automatische Resets
|
|
218
239
|
|
|
219
240
|
Der Adapter setzt Zähler automatisch zurück:
|
|
@@ -228,6 +249,12 @@ Der Adapter setzt Zähler automatisch zurück:
|
|
|
228
249
|
|
|
229
250
|
## Changelog
|
|
230
251
|
|
|
252
|
+
### 1.3.3 (2026-01-09)
|
|
253
|
+
|
|
254
|
+
- **NEW:** **CSV-Import** - Importiere historische Daten (z.B. aus der EhB+ App) für Strom, Gas, Wasser und PV.
|
|
255
|
+
- **NEW:** **Benutzerdefinierte Zähler** - Unterstützung für Zwischenzähler (z.B. Gartenhaus, Einliegerwohnung).
|
|
256
|
+
- **IMPROVED:** Import-UI optimiert (Icons, Button-Layout).
|
|
257
|
+
|
|
231
258
|
### 1.3.2 (2026-01-09)
|
|
232
259
|
|
|
233
260
|
- **NEW:** **PV / Einspeise-Unterstützung** ☀️ - Neuer Tab für Photovoltaik:
|
package/admin/jsonConfig.json
CHANGED
|
@@ -803,7 +803,7 @@
|
|
|
803
803
|
"tabPv": {
|
|
804
804
|
"type": "panel",
|
|
805
805
|
"label": "PV / Einspeisung",
|
|
806
|
-
"icon": "data:image/svg+xml;base64,
|
|
806
|
+
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJMNi43NiA0Ljg0bC0xLjgtMS43OS0xLjQxIDEuNDEgMS43OSAxLjc5IDEuNDItMS40MXpNNCAxMC41SDF2M2gzdi0zem05LTkuOTVoLTJWMy41aDJWLjU1em03LjQ1IDMuOTFsLTEuNDEtMS40MS0xLjc5IDEuNzkgMS40MSAxLjQxIDEuNzktMS43OXptLTMuMjEgMTMuN2wxLjc5IDEuOCAxLjQxLTEuNDEtMS44LTEuNzktMS40IDEuNHoyMCAxMC41djNoM3diLTNoLTN6bS04LTVjLTMuMzEgMC02IDIuNjktNiA2czIuNjkgNiA2IDYgNi0yLjY5IDYtNi0yLjY5LTYtNi02em0tMSAxNi45NWgyVjE5LjVoLTJ2Mi45NXptLTcuNDUtMy45MWwxLjQxIDEuNDEgMS43OS0xLjgtMS40MS0xLjQxLTEuNzkgMS44eiIvPjwvc3ZnPg==",
|
|
807
807
|
"items": {
|
|
808
808
|
"_pvActivationHeader": {
|
|
809
809
|
"type": "header",
|
|
@@ -1120,6 +1120,107 @@
|
|
|
1120
1120
|
}
|
|
1121
1121
|
}
|
|
1122
1122
|
},
|
|
1123
|
+
"tabImport": {
|
|
1124
|
+
"type": "panel",
|
|
1125
|
+
"label": "Import",
|
|
1126
|
+
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMCAwaDI0djI0SDBWMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMTkgOWgtNFYzSDl2Nkg1bDcgNyA3LTd6TTUgMTh2MmgxNHdiLTJINXoiLz48L3N2Zz4=",
|
|
1127
|
+
"items": {
|
|
1128
|
+
"_importHeader": {
|
|
1129
|
+
"type": "header",
|
|
1130
|
+
"text": "Historische Daten importieren",
|
|
1131
|
+
"size": 2
|
|
1132
|
+
},
|
|
1133
|
+
"_importDesc": {
|
|
1134
|
+
"type": "staticText",
|
|
1135
|
+
"text": "Hier kannst du alte Zählerstände aus CSV-Dateien (z.B. EhB+ App) importieren. Die Daten werden in die Historie (Jahresverbrauch) geschrieben. Bitte kopiere den Inhalt der CSV-Datei in das Textfeld.",
|
|
1136
|
+
"xs": 12
|
|
1137
|
+
},
|
|
1138
|
+
"importTarget": {
|
|
1139
|
+
"type": "select",
|
|
1140
|
+
"label": "Ziel-Medium",
|
|
1141
|
+
"options": [
|
|
1142
|
+
{ "label": "Bitte wählen...", "value": "" },
|
|
1143
|
+
{ "label": "Strom", "value": "electricity" },
|
|
1144
|
+
{ "label": "Gas", "value": "gas" },
|
|
1145
|
+
{ "label": "Wasser", "value": "water" },
|
|
1146
|
+
{ "label": "PV / Einspeisung", "value": "pv" },
|
|
1147
|
+
{ "label": "Benutzerdefiniert / Zwischenzähler", "value": "custom" }
|
|
1148
|
+
],
|
|
1149
|
+
"default": "",
|
|
1150
|
+
"xs": 12,
|
|
1151
|
+
"sm": 12,
|
|
1152
|
+
"md": 6,
|
|
1153
|
+
"lg": 4,
|
|
1154
|
+
"xl": 4
|
|
1155
|
+
},
|
|
1156
|
+
"importCustomName": {
|
|
1157
|
+
"type": "text",
|
|
1158
|
+
"label": "Name des Zählers",
|
|
1159
|
+
"placeholder": "z.B. Einliegerwohnung",
|
|
1160
|
+
"hidden": "data.importTarget !== 'custom'",
|
|
1161
|
+
"xs": 12,
|
|
1162
|
+
"sm": 12,
|
|
1163
|
+
"md": 6,
|
|
1164
|
+
"lg": 4,
|
|
1165
|
+
"xl": 4
|
|
1166
|
+
},
|
|
1167
|
+
"importUnit": {
|
|
1168
|
+
"type": "select",
|
|
1169
|
+
"label": "Einheit",
|
|
1170
|
+
"options": [
|
|
1171
|
+
{ "label": "kWh", "value": "kWh" },
|
|
1172
|
+
{ "label": "m³", "value": "m3" }
|
|
1173
|
+
],
|
|
1174
|
+
"default": "kWh",
|
|
1175
|
+
"hidden": "data.importTarget !== 'custom'",
|
|
1176
|
+
"xs": 12,
|
|
1177
|
+
"sm": 12,
|
|
1178
|
+
"md": 6,
|
|
1179
|
+
"lg": 4,
|
|
1180
|
+
"xl": 4
|
|
1181
|
+
},
|
|
1182
|
+
"importFormat": {
|
|
1183
|
+
"type": "select",
|
|
1184
|
+
"label": "Quell-Format",
|
|
1185
|
+
"options": [{ "label": "EhB+ App (CSV)", "value": "ehb" }],
|
|
1186
|
+
"default": "ehb",
|
|
1187
|
+
"xs": 12,
|
|
1188
|
+
"sm": 12,
|
|
1189
|
+
"md": 6,
|
|
1190
|
+
"lg": 4,
|
|
1191
|
+
"xl": 4
|
|
1192
|
+
},
|
|
1193
|
+
"importContent": {
|
|
1194
|
+
"type": "text",
|
|
1195
|
+
"label": "CSV Inhalt (hier einfügen)",
|
|
1196
|
+
"rows": 10,
|
|
1197
|
+
"xs": 12,
|
|
1198
|
+
"sm": 12,
|
|
1199
|
+
"md": 12,
|
|
1200
|
+
"lg": 12,
|
|
1201
|
+
"xl": 12,
|
|
1202
|
+
"noCreate": true
|
|
1203
|
+
},
|
|
1204
|
+
"btnImport": {
|
|
1205
|
+
"type": "sendTo",
|
|
1206
|
+
"label": "Daten importieren",
|
|
1207
|
+
"command": "importData",
|
|
1208
|
+
"jsonData": "{\"utility\": \"${data.importTarget}\", \"type\": \"${data.importFormat}\", \"content\": \"${data.importContent}\", \"customName\": \"${data.importCustomName}\", \"unit\": \"${data.importUnit}\"}",
|
|
1209
|
+
"disabled": "!data.importTarget || !data.importContent",
|
|
1210
|
+
"variant": "outlined",
|
|
1211
|
+
"style": {
|
|
1212
|
+
"marginTop": "10px",
|
|
1213
|
+
"width": "100%"
|
|
1214
|
+
},
|
|
1215
|
+
"xs": 12,
|
|
1216
|
+
"sm": 12,
|
|
1217
|
+
"md": 4,
|
|
1218
|
+
"lg": 4,
|
|
1219
|
+
"xl": 4,
|
|
1220
|
+
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMjRweCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMCAwaDI0djI0SDBWMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMTkgOWgtNFYzSDl2Nkg1bDcgNyA3LTd6TTUgMTh2MmgxNHdiLTJINXoiLz48L3N2Zz4="
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1123
1224
|
"tabInfo": {
|
|
1124
1225
|
"type": "panel",
|
|
1125
1226
|
"label": "ℹ️ Info & Hilfe",
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "nebenkosten-monitor",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.3",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.3.3": {
|
|
7
|
+
"en": "New: CSV Import feature for historical data (e.g. EhB+ App). New: Support for Custom Meters (intermediate meters).",
|
|
8
|
+
"de": "Neu: CSV-Import-Funktion für historische Daten (z.B. EhB+ App). Neu: Unterstützung für benutzerdefinierte Zähler (Zwischenzähler)."
|
|
9
|
+
},
|
|
6
10
|
"1.3.2": {
|
|
7
11
|
"en": "New: PV/Feed-in integration! Monitor your solar feed-in and earnings. New: Notifications for PV. Improved: Reorganized config UI.",
|
|
8
12
|
"de": "Neu: PV/Einspeise-Integration! Überwache deine Solareinspeisung und Vergütung. Neu: Benachrichtigungen für PV. Verbessert: Aufgeräumte Konfigurations-Oberfläche."
|
|
@@ -18,14 +22,6 @@
|
|
|
18
22
|
"1.2.7": {
|
|
19
23
|
"en": "New: Universal notification system for reminders. New: PayPal donation support. Fix: Improved precision for daily consumption.",
|
|
20
24
|
"de": "Neu: Universelles Benachrichtigungssystem für Erinnerungen. Neu: PayPal-Spendenunterstützung. Fix: Verbesserte Präzision für den Tagesverbrauch."
|
|
21
|
-
},
|
|
22
|
-
"1.2.6": {
|
|
23
|
-
"en": "Fix: Allow empty basic charge fields in configuration (default to 0)",
|
|
24
|
-
"de": "Fix: Grundgebühr-Felder dürfen in der Konfiguration leer sein (Standard 0)"
|
|
25
|
-
},
|
|
26
|
-
"1.2.5": {
|
|
27
|
-
"en": "Stability update: Fixed critical consumption delta calculation bug",
|
|
28
|
-
"de": "Stabilitäts-Update: Kritischen Fehler in der Verbrauchs-Delta-Berechnung behoben"
|
|
29
25
|
}
|
|
30
26
|
},
|
|
31
27
|
"titleLang": {
|
|
@@ -112,6 +108,14 @@
|
|
|
112
108
|
"stromPreis": 0,
|
|
113
109
|
"stromGrundgebuehr": 0,
|
|
114
110
|
"stromAbschlag": 0,
|
|
111
|
+
"pvAktiv": false,
|
|
112
|
+
"pvSensorDP": "",
|
|
113
|
+
"pvOffset": 0,
|
|
114
|
+
"pvInitialReading": 0,
|
|
115
|
+
"pvContractStart": "",
|
|
116
|
+
"pvPreis": 0,
|
|
117
|
+
"pvGrundgebuehr": 0,
|
|
118
|
+
"pvJahresgebuehr": 0,
|
|
115
119
|
"notificationEnabled": false,
|
|
116
120
|
"notificationInstance": "",
|
|
117
121
|
"notificationDaysBefore": 30,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EhbImporter = require('./importers/ehb');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ImportManager handles data import from different sources
|
|
7
|
+
*/
|
|
8
|
+
class ImportManager {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} adapter - ioBroker adapter instance
|
|
11
|
+
*/
|
|
12
|
+
constructor(adapter) {
|
|
13
|
+
this.adapter = adapter;
|
|
14
|
+
this.importers = {
|
|
15
|
+
ehb: new EhbImporter(adapter),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* handle import message
|
|
21
|
+
*
|
|
22
|
+
* @param {object} msg - The message object
|
|
23
|
+
*/
|
|
24
|
+
async handleImportMessage(msg) {
|
|
25
|
+
try {
|
|
26
|
+
let { utility, type, content, customName, unit } = msg.message; // utility='gas', type='ehb', content='...'
|
|
27
|
+
|
|
28
|
+
if (utility === 'custom') {
|
|
29
|
+
if (!customName) {
|
|
30
|
+
throw new Error('Name für benutzerdefinierten Zähler fehlt.');
|
|
31
|
+
}
|
|
32
|
+
// Sanitize name: "Garten Haus" -> "garten_haus"
|
|
33
|
+
utility = customName
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/\s+/g, '_')
|
|
36
|
+
.replace(/[^a-z0-9_]/g, '');
|
|
37
|
+
|
|
38
|
+
if (utility.length === 0) {
|
|
39
|
+
throw new Error('Ungültiger Name für Zähler.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Ensure unit is valid (default to kWh if something weird comes in)
|
|
43
|
+
if (unit === 'm3') {
|
|
44
|
+
unit = 'm³'; // Fix mapping from UI value
|
|
45
|
+
}
|
|
46
|
+
if (!unit) {
|
|
47
|
+
unit = 'kWh';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!this.importers[type]) {
|
|
52
|
+
throw new Error(`Unknown importer type: ${type}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.adapter.log.info(`[Import] Starting import for ${utility} using ${type}...`);
|
|
56
|
+
const records = await this.importers[type].parse(content);
|
|
57
|
+
|
|
58
|
+
if (!records || records.length === 0) {
|
|
59
|
+
return { error: 'No valid data found in CSV.' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = await this.processRecords(utility, records, unit);
|
|
63
|
+
|
|
64
|
+
const details = result.details.map(d => `${d.year}: ${d.consumption.toFixed(2)} ${d.unit}`).join(', ');
|
|
65
|
+
this.adapter.log.info(`[Import] Details: ${details}`);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
result: `Erfolgreich importiert: ${records.length} Datensätze verarbeitet.`,
|
|
69
|
+
native: {
|
|
70
|
+
importContent: '',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
this.adapter.log.error(`[Import] Error: ${error.message}`);
|
|
75
|
+
return { error: error.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Process records and write to history
|
|
81
|
+
*
|
|
82
|
+
* @param {string} utility - The utility type (gas, water, electricity, pv, or custom name)
|
|
83
|
+
* @param {Array<{timestamp: number, value: number, dateObj: Date}>} records - Parsed records
|
|
84
|
+
* @param {string} [customUnit] - Unit for custom meters
|
|
85
|
+
*/
|
|
86
|
+
async processRecords(utility, records, customUnit) {
|
|
87
|
+
// Group by year
|
|
88
|
+
const years = {};
|
|
89
|
+
|
|
90
|
+
for (const r of records) {
|
|
91
|
+
const year = r.dateObj.getFullYear();
|
|
92
|
+
if (!years[year]) {
|
|
93
|
+
years[year] = [];
|
|
94
|
+
}
|
|
95
|
+
years[year].push(r);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// eslint-disable-next-line jsdoc/check-tag-names
|
|
99
|
+
/** @type {{ yearsUpdated: string[], details: Array<{year: number, consumption: number, unit: string}> }} */
|
|
100
|
+
const stats = { yearsUpdated: [], details: [] };
|
|
101
|
+
|
|
102
|
+
for (const year of Object.keys(years)) {
|
|
103
|
+
const readings = years[year];
|
|
104
|
+
// Sort just in case
|
|
105
|
+
readings.sort((a, b) => a.timestamp - b.timestamp);
|
|
106
|
+
|
|
107
|
+
const startVal = readings[0].value;
|
|
108
|
+
const endVal = readings[readings.length - 1].value;
|
|
109
|
+
let consumption = endVal - startVal;
|
|
110
|
+
|
|
111
|
+
if (consumption < 0) {
|
|
112
|
+
consumption = 0; // Reset handling? simpler for now.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let unit = customUnit || 'kWh'; // Default or custom
|
|
116
|
+
|
|
117
|
+
// Gas: Convert m3 to kWh? (Only for standard 'gas' utility)
|
|
118
|
+
if (utility === 'gas') {
|
|
119
|
+
unit = 'kWh'; // Gas is always kWh in history despite input
|
|
120
|
+
|
|
121
|
+
// Read current conversion factors (Not perfect for history, but better than nothing)
|
|
122
|
+
const brennwert = this.adapter.config.gasBrennwert || 1;
|
|
123
|
+
const zustandszahl = this.adapter.config.gasZahl || 1;
|
|
124
|
+
|
|
125
|
+
// Save Volume
|
|
126
|
+
await this.adapter.setObjectNotExistsAsync(`${utility}.history.${year}.yearlyVolume`, {
|
|
127
|
+
type: 'state',
|
|
128
|
+
common: { name: `Verbrauch ${year} in m³`, type: 'number', unit: 'm³', role: 'value' },
|
|
129
|
+
native: {},
|
|
130
|
+
});
|
|
131
|
+
await this.adapter.setStateAsync(`${utility}.history.${year}.yearlyVolume`, consumption, true);
|
|
132
|
+
|
|
133
|
+
// Convert to kWh
|
|
134
|
+
consumption = consumption * brennwert * zustandszahl;
|
|
135
|
+
} else if (utility === 'water') {
|
|
136
|
+
unit = 'm³';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Write Yearly Consumption
|
|
140
|
+
await this.adapter.setObjectNotExistsAsync(`${utility}.history.${year}`, {
|
|
141
|
+
type: 'channel',
|
|
142
|
+
common: { name: `Jahr ${year}` },
|
|
143
|
+
native: {},
|
|
144
|
+
});
|
|
145
|
+
await this.adapter.setObjectNotExistsAsync(`${utility}.history.${year}.yearly`, {
|
|
146
|
+
type: 'state',
|
|
147
|
+
common: { name: `Jahresverbrauch ${year}`, type: 'number', unit: unit, role: 'value' },
|
|
148
|
+
native: {},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const stateId = `${utility}.history.${year}.yearly`;
|
|
152
|
+
await this.adapter.setStateAsync(stateId, consumption, true);
|
|
153
|
+
|
|
154
|
+
stats.yearsUpdated.push(year);
|
|
155
|
+
stats.details.push({ year: parseInt(year), consumption, unit });
|
|
156
|
+
|
|
157
|
+
this.adapter.log.info(
|
|
158
|
+
`[Import] ${utility} ${year}: ${consumption.toFixed(2)} ${unit} (from ${startVal} to ${endVal})`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return stats;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = ImportManager;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Abstract base class for importers
|
|
5
|
+
*/
|
|
6
|
+
class AbstractImporter {
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} adapter - Adapter instance
|
|
9
|
+
*/
|
|
10
|
+
constructor(adapter) {
|
|
11
|
+
this.adapter = adapter;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses the CSV content
|
|
16
|
+
*
|
|
17
|
+
* @param {string} _content - Raw CSV string
|
|
18
|
+
* @returns {Promise<Array<{timestamp: number, value: number}>>} - Array of objects with timestamp and value
|
|
19
|
+
*/
|
|
20
|
+
async parse(_content) {
|
|
21
|
+
throw new Error('Method "parse" must be implemented');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validates if the content matches this importer
|
|
26
|
+
*
|
|
27
|
+
* @param {string} _content - Raw content to validate
|
|
28
|
+
* @returns {boolean} - True if valid, false otherwise
|
|
29
|
+
*/
|
|
30
|
+
validate(_content) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = AbstractImporter;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const AbstractImporter = require('./abstractImporter');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Importer for "Energie Haushalts Buch" (EhB) App
|
|
7
|
+
* Format assumption: German CSV (semicolon separator)
|
|
8
|
+
* Date;Value;Comment
|
|
9
|
+
* 01.01.2023 12:00;1234.5;Ablesung
|
|
10
|
+
*/
|
|
11
|
+
class EhbImporter extends AbstractImporter {
|
|
12
|
+
/**
|
|
13
|
+
* Parse the CSV content
|
|
14
|
+
*
|
|
15
|
+
* @param {string} content - Raw content
|
|
16
|
+
* @returns {Promise<Array<any>>} - Parsed results
|
|
17
|
+
*/
|
|
18
|
+
async parse(content) {
|
|
19
|
+
const results = [];
|
|
20
|
+
// Regex to match: Date;Value;Comment
|
|
21
|
+
// Supports:
|
|
22
|
+
// DD.MM.YYYY or DD.MM.YYYY HH:mm
|
|
23
|
+
// Semicolon separator
|
|
24
|
+
// Value with dot or comma
|
|
25
|
+
// Optional comment
|
|
26
|
+
const regex = /(\d{2}\.\d{2}\.\d{4}(?:\s+\d{2}:\d{2}(?::\d{2})?)?)\s*;\s*(\d+(?:[.,]\d+)?)/g;
|
|
27
|
+
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = regex.exec(content)) !== null) {
|
|
30
|
+
const dateStr = match[1].trim();
|
|
31
|
+
const valueStr = match[2].trim().replace(',', '.');
|
|
32
|
+
|
|
33
|
+
const date = this.parseGermanDate(dateStr);
|
|
34
|
+
const value = parseFloat(valueStr);
|
|
35
|
+
|
|
36
|
+
if (date && !isNaN(value)) {
|
|
37
|
+
results.push({
|
|
38
|
+
timestamp: date.getTime(),
|
|
39
|
+
value: value,
|
|
40
|
+
dateObj: date,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sort by date ascending
|
|
46
|
+
if (results.length > 0) {
|
|
47
|
+
results.sort((a, b) => a.timestamp - b.timestamp);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse german date string
|
|
55
|
+
*
|
|
56
|
+
* @param {string} dateStr - Date string
|
|
57
|
+
* @returns {Date|null} - Date object or null
|
|
58
|
+
*/
|
|
59
|
+
parseGermanDate(dateStr) {
|
|
60
|
+
try {
|
|
61
|
+
// Check for time component
|
|
62
|
+
let timeStr = '00:00:00';
|
|
63
|
+
let dStr = dateStr;
|
|
64
|
+
|
|
65
|
+
if (dateStr.includes(' ')) {
|
|
66
|
+
const parts = dateStr.split(/\s+/); // Handle multiple spaces
|
|
67
|
+
dStr = parts[0];
|
|
68
|
+
if (parts[1]) {
|
|
69
|
+
timeStr = parts[1];
|
|
70
|
+
// Add seconds if missing
|
|
71
|
+
if (timeStr.split(':').length === 2) {
|
|
72
|
+
timeStr += ':00';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const [day, month, year] = dStr.split('.');
|
|
78
|
+
// Simple check
|
|
79
|
+
if (!day || !month || !year) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create ISO string YYYY-MM-DDTHH:mm:ss
|
|
84
|
+
return new Date(`${year}-${month}-${day}T${timeStr}`);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = EhbImporter;
|
package/main.js
CHANGED
|
@@ -9,6 +9,7 @@ const utils = require('@iobroker/adapter-core');
|
|
|
9
9
|
const ConsumptionManager = require('./lib/consumptionManager');
|
|
10
10
|
const BillingManager = require('./lib/billingManager');
|
|
11
11
|
const MessagingHandler = require('./lib/messagingHandler');
|
|
12
|
+
const ImportManager = require('./lib/importManager');
|
|
12
13
|
|
|
13
14
|
class NebenkostenMonitor extends utils.Adapter {
|
|
14
15
|
/**
|
|
@@ -28,6 +29,7 @@ class NebenkostenMonitor extends utils.Adapter {
|
|
|
28
29
|
this.consumptionManager = new ConsumptionManager(this);
|
|
29
30
|
this.billingManager = new BillingManager(this);
|
|
30
31
|
this.messagingHandler = new MessagingHandler(this);
|
|
32
|
+
this.importManager = new ImportManager(this);
|
|
31
33
|
|
|
32
34
|
this.periodicTimers = {};
|
|
33
35
|
}
|
|
@@ -188,7 +190,14 @@ class NebenkostenMonitor extends utils.Adapter {
|
|
|
188
190
|
* @param {Record<string, any>} obj - Message object from config
|
|
189
191
|
*/
|
|
190
192
|
async onMessage(obj) {
|
|
191
|
-
|
|
193
|
+
if (obj.command === 'importData') {
|
|
194
|
+
const result = await this.importManager.handleImportMessage(obj);
|
|
195
|
+
if (obj.callback) {
|
|
196
|
+
this.sendTo(obj.from, obj.command, result, obj.callback);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
await this.messagingHandler.handleMessage(obj);
|
|
200
|
+
}
|
|
192
201
|
}
|
|
193
202
|
}
|
|
194
203
|
|