iobroker.utility-monitor 1.4.4 → 1.4.6
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 +95 -63
- package/admin/jsonConfig.json +130 -39
- package/io-package.json +25 -51
- package/lib/billingManager.js +53 -53
- package/lib/consumptionManager.js +63 -103
- package/lib/messagingHandler.js +108 -109
- package/lib/multiMeterManager.js +48 -68
- package/lib/stateManager.js +6 -5
- package/main.js +31 -45
- package/package.json +13 -13
- package/admin/tab_m.html +0 -305
- package/lib/importManager.js +0 -344
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.utility-monitor",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
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;
|