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/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
|
/**
|
|
@@ -27,7 +24,36 @@ class MessagingHandler {
|
|
|
27
24
|
|
|
28
25
|
this.adapter.log.debug(`[onMessage] Received command: ${obj.command} from ${obj.from}`);
|
|
29
26
|
|
|
30
|
-
if (obj.command === '
|
|
27
|
+
if (obj.command === 'getMeters') {
|
|
28
|
+
try {
|
|
29
|
+
// Get the utility type from the message
|
|
30
|
+
const type = obj.message?.type;
|
|
31
|
+
|
|
32
|
+
if (!type) {
|
|
33
|
+
this.adapter.sendTo(obj.from, obj.command, [], obj.callback);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get all meters for this type from multiMeterManager
|
|
38
|
+
const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
|
|
39
|
+
|
|
40
|
+
if (meters.length === 0) {
|
|
41
|
+
this.adapter.sendTo(obj.from, obj.command, [], obj.callback);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Build options array: [{ value: "main", label: "Hauptzähler (main)" }, ...]
|
|
46
|
+
const result = meters.map(meter => ({
|
|
47
|
+
value: meter.name,
|
|
48
|
+
label: meter.displayName ? `${meter.displayName} (${meter.name})` : meter.name,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
this.adapter.sendTo(obj.from, obj.command, result, obj.callback);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.adapter.log.error(`Error in getMeters callback: ${error.message}`);
|
|
54
|
+
this.adapter.sendTo(obj.from, obj.command, [], obj.callback);
|
|
55
|
+
}
|
|
56
|
+
} else if (obj.command === 'getInstances') {
|
|
31
57
|
try {
|
|
32
58
|
const instances = await this.adapter.getForeignObjectsAsync('system.adapter.*', 'instance');
|
|
33
59
|
const messengerTypes = [
|
|
@@ -139,65 +165,6 @@ class MessagingHandler {
|
|
|
139
165
|
);
|
|
140
166
|
}
|
|
141
167
|
}
|
|
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
168
|
} else {
|
|
202
169
|
this.adapter.log.warn(`[onMessage] Unknown command: ${obj.command}`);
|
|
203
170
|
if (obj.callback) {
|
|
@@ -225,43 +192,66 @@ class MessagingHandler {
|
|
|
225
192
|
continue;
|
|
226
193
|
}
|
|
227
194
|
|
|
228
|
-
// Get
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
`📅 Datum: ${periodEnd}\n\n` +
|
|
244
|
-
`Bitte trage den Zählerstand rechtzeitig ein:\n` +
|
|
245
|
-
`1️⃣ Datenpunkt: ${type}.billing.endReading\n` +
|
|
246
|
-
`2️⃣ Zeitraum abschließen: ${type}.billing.closePeriod = true`;
|
|
247
|
-
|
|
248
|
-
await this.sendNotification(type, message, 'billing');
|
|
249
|
-
}
|
|
195
|
+
// Get all meters for this type (main + additional)
|
|
196
|
+
const allMeters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
|
|
197
|
+
if (allMeters.length === 0) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Filter meters based on configuration
|
|
202
|
+
// If notificationXXXMeters is empty or undefined, notify ALL meters
|
|
203
|
+
const configKey = `notification${configType.charAt(0).toUpperCase() + configType.slice(1)}Meters`;
|
|
204
|
+
const selectedMeters = this.adapter.config[configKey];
|
|
205
|
+
|
|
206
|
+
let metersToNotify = allMeters;
|
|
207
|
+
if (selectedMeters && Array.isArray(selectedMeters) && selectedMeters.length > 0) {
|
|
208
|
+
// Filter: only notify selected meters
|
|
209
|
+
metersToNotify = allMeters.filter(meter => selectedMeters.includes(meter.name));
|
|
250
210
|
}
|
|
251
211
|
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
const
|
|
212
|
+
// Check notifications for each meter individually
|
|
213
|
+
for (const meter of metersToNotify) {
|
|
214
|
+
const basePath = `${type}.${meter.name}`;
|
|
215
|
+
const meterLabel = meter.displayName || meter.name;
|
|
216
|
+
|
|
217
|
+
// Get current days remaining for THIS meter
|
|
218
|
+
const daysRemainingState = await this.adapter.getStateAsync(`${basePath}.billing.daysRemaining`);
|
|
219
|
+
const daysRemaining = typeof daysRemainingState?.val === 'number' ? daysRemainingState.val : 999;
|
|
220
|
+
const periodEndState = await this.adapter.getStateAsync(`${basePath}.billing.periodEnd`);
|
|
221
|
+
const periodEnd = periodEndState?.val || '--.--.----';
|
|
222
|
+
|
|
223
|
+
// 1. BILLING END REMINDER (Zählerstand ablesen)
|
|
224
|
+
if (this.adapter.config.notificationBillingEnabled) {
|
|
225
|
+
const billingSent = await this.adapter.getStateAsync(`${basePath}.billing.notificationSent`);
|
|
226
|
+
const billingDaysThreshold = this.adapter.config.notificationBillingDays || 7;
|
|
227
|
+
|
|
228
|
+
if (billingSent?.val !== true && daysRemaining <= billingDaysThreshold) {
|
|
229
|
+
const message =
|
|
230
|
+
`🔔 *Nebenkosten-Monitor: Zählerstand ablesen*\n\n` +
|
|
231
|
+
`Dein Abrechnungszeitraum für *${typesDe[type]} (${meterLabel})* endet in ${daysRemaining} Tagen!\n\n` +
|
|
232
|
+
`📅 Datum: ${periodEnd}\n\n` +
|
|
233
|
+
`Bitte trage den Zählerstand rechtzeitig ein:\n` +
|
|
234
|
+
`1️⃣ Datenpunkt: ${basePath}.billing.endReading\n` +
|
|
235
|
+
`2️⃣ Zeitraum abschließen: ${basePath}.billing.closePeriod = true`;
|
|
236
|
+
|
|
237
|
+
await this.sendNotification(basePath, message, 'billing');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 2. CONTRACT CHANGE REMINDER (Tarif wechseln / Kündigungsfrist)
|
|
242
|
+
if (this.adapter.config.notificationChangeEnabled) {
|
|
243
|
+
const changeSent = await this.adapter.getStateAsync(`${basePath}.billing.notificationChangeSent`);
|
|
244
|
+
const changeDaysThreshold = this.adapter.config.notificationChangeDays || 60;
|
|
256
245
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
246
|
+
if (changeSent?.val !== true && daysRemaining <= changeDaysThreshold) {
|
|
247
|
+
const message =
|
|
248
|
+
`💡 *Nebenkosten-Monitor: Tarif-Check*\n\n` +
|
|
249
|
+
`Dein Vertrag für *${typesDe[type]} (${meterLabel})* endet am ${periodEnd}.\n\n` +
|
|
250
|
+
`⏰ Noch ${daysRemaining} Tage bis zum Ende des Zeitraums.\n\n` +
|
|
251
|
+
`Jetzt ist ein guter Zeitpunkt, um Preise zu vergleichen oder die Kündigungsfrist zu prüfen! 💸`;
|
|
263
252
|
|
|
264
|
-
|
|
253
|
+
await this.sendNotification(basePath, message, 'change');
|
|
254
|
+
}
|
|
265
255
|
}
|
|
266
256
|
}
|
|
267
257
|
}
|
|
@@ -320,16 +310,24 @@ class MessagingHandler {
|
|
|
320
310
|
// Multi-meter: use totals
|
|
321
311
|
yearlyState = await this.adapter.getStateAsync(`${type}.totals.consumption.yearly`);
|
|
322
312
|
totalYearlyState = await this.adapter.getStateAsync(`${type}.totals.costs.totalYearly`);
|
|
323
|
-
// Balance/paidTotal not available in totals, use
|
|
324
|
-
|
|
325
|
-
|
|
313
|
+
// Balance/paidTotal not available in totals, use first meter as representative
|
|
314
|
+
const firstMeter = meters[0];
|
|
315
|
+
if (firstMeter) {
|
|
316
|
+
paidTotalState = await this.adapter.getStateAsync(`${type}.${firstMeter.name}.costs.paidTotal`);
|
|
317
|
+
balanceState = await this.adapter.getStateAsync(`${type}.${firstMeter.name}.costs.balance`);
|
|
318
|
+
}
|
|
326
319
|
message += `(${meters.length} Zähler gesamt)\\n`;
|
|
320
|
+
} else if (meters.length === 1) {
|
|
321
|
+
// Single meter: use first meter values (new path structure)
|
|
322
|
+
const meter = meters[0];
|
|
323
|
+
const basePath = `${type}.${meter.name}`;
|
|
324
|
+
yearlyState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
|
|
325
|
+
totalYearlyState = await this.adapter.getStateAsync(`${basePath}.costs.totalYearly`);
|
|
326
|
+
paidTotalState = await this.adapter.getStateAsync(`${basePath}.costs.paidTotal`);
|
|
327
|
+
balanceState = await this.adapter.getStateAsync(`${basePath}.costs.balance`);
|
|
327
328
|
} else {
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
totalYearlyState = await this.adapter.getStateAsync(`${type}.costs.totalYearly`);
|
|
331
|
-
paidTotalState = await this.adapter.getStateAsync(`${type}.costs.paidTotal`);
|
|
332
|
-
balanceState = await this.adapter.getStateAsync(`${type}.costs.balance`);
|
|
329
|
+
// No meters configured - skip this type
|
|
330
|
+
continue;
|
|
333
331
|
}
|
|
334
332
|
|
|
335
333
|
let val = yearlyState?.val || 0;
|
|
@@ -372,14 +370,14 @@ class MessagingHandler {
|
|
|
372
370
|
/**
|
|
373
371
|
* Helper to send notification and mark as sent
|
|
374
372
|
*
|
|
375
|
-
* @param {string}
|
|
373
|
+
* @param {string} pathOrType - Full path like "gas.main" or just "system" for reports
|
|
376
374
|
* @param {string} message - Message text
|
|
377
375
|
* @param {string} reminderType - billing, change, or report
|
|
378
376
|
*/
|
|
379
|
-
async sendNotification(
|
|
377
|
+
async sendNotification(pathOrType, message, reminderType) {
|
|
380
378
|
try {
|
|
381
379
|
const instance = this.adapter.config.notificationInstance;
|
|
382
|
-
this.adapter.log.info(`Sending ${reminderType} notification for ${
|
|
380
|
+
this.adapter.log.info(`Sending ${reminderType} notification for ${pathOrType} via ${instance}`);
|
|
383
381
|
|
|
384
382
|
await this.adapter.sendToAsync(instance, 'send', {
|
|
385
383
|
text: message,
|
|
@@ -388,12 +386,13 @@ class MessagingHandler {
|
|
|
388
386
|
});
|
|
389
387
|
|
|
390
388
|
// Mark as sent (only for billing/change)
|
|
389
|
+
// pathOrType is now the full path like "gas.main" or "gas.werkstatt"
|
|
391
390
|
if (reminderType !== 'report') {
|
|
392
391
|
const stateKey = reminderType === 'change' ? 'notificationChangeSent' : 'notificationSent';
|
|
393
|
-
await this.adapter.setStateAsync(`${
|
|
392
|
+
await this.adapter.setStateAsync(`${pathOrType}.billing.${stateKey}`, true, true);
|
|
394
393
|
}
|
|
395
394
|
} catch (error) {
|
|
396
|
-
this.adapter.log.error(`Failed to send ${reminderType} notification for ${
|
|
395
|
+
this.adapter.log.error(`Failed to send ${reminderType} notification for ${pathOrType}: ${error.message}`);
|
|
397
396
|
}
|
|
398
397
|
}
|
|
399
398
|
}
|
package/lib/multiMeterManager.js
CHANGED
|
@@ -70,8 +70,14 @@ class MultiMeterManager {
|
|
|
70
70
|
// Main meter (always present if type is active)
|
|
71
71
|
const mainActive = this.adapter.config[`${configType}Aktiv`];
|
|
72
72
|
if (mainActive) {
|
|
73
|
+
// Get main meter name from config and normalize
|
|
74
|
+
const mainMeterName = this.adapter.config[`${configType}MainMeterName`] || 'main';
|
|
75
|
+
const normalizedName = this.normalizeMeterName(mainMeterName);
|
|
76
|
+
const displayName = mainMeterName; // Original name for display
|
|
77
|
+
|
|
73
78
|
meters.push({
|
|
74
|
-
name:
|
|
79
|
+
name: normalizedName,
|
|
80
|
+
displayName: displayName,
|
|
75
81
|
config: {
|
|
76
82
|
sensorDP: this.adapter.config[`${configType}SensorDP`],
|
|
77
83
|
preis: parseConfigNumber(this.adapter.config[`${configType}Preis`], 0),
|
|
@@ -173,7 +179,7 @@ class MultiMeterManager {
|
|
|
173
179
|
* @returns {Promise<void>}
|
|
174
180
|
*/
|
|
175
181
|
async initializeMeter(type, meterName, config, displayName) {
|
|
176
|
-
const basePath =
|
|
182
|
+
const basePath = `${type}.${meterName}`;
|
|
177
183
|
const label = displayName || meterName;
|
|
178
184
|
|
|
179
185
|
this.adapter.log.info(`Initializing ${type} meter: ${label}`);
|
|
@@ -349,7 +355,7 @@ class MultiMeterManager {
|
|
|
349
355
|
return;
|
|
350
356
|
}
|
|
351
357
|
|
|
352
|
-
const basePath =
|
|
358
|
+
const basePath = `${type}.${meterName}`;
|
|
353
359
|
this.adapter.log.debug(`Sensor update for ${basePath}: ${value}`);
|
|
354
360
|
|
|
355
361
|
// Get meter config
|
|
@@ -491,14 +497,14 @@ class MultiMeterManager {
|
|
|
491
497
|
* @param {object} config - Meter configuration
|
|
492
498
|
*/
|
|
493
499
|
async updateCurrentPrice(type, meterName, config) {
|
|
494
|
-
const basePath =
|
|
500
|
+
const basePath = `${type}.${meterName}`;
|
|
495
501
|
const configType = this.getConfigType(type);
|
|
496
502
|
|
|
497
503
|
let tariffName = 'Standard';
|
|
498
504
|
let activePrice = config.preis || 0;
|
|
499
505
|
|
|
500
|
-
// Only
|
|
501
|
-
if (
|
|
506
|
+
// Only meters with HT/NT enabled support it
|
|
507
|
+
if (config.htNtEnabled) {
|
|
502
508
|
const isHT = calculator.isHTTime(this.adapter.config, configType);
|
|
503
509
|
if (isHT) {
|
|
504
510
|
activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
|
|
@@ -526,7 +532,7 @@ class MultiMeterManager {
|
|
|
526
532
|
* @param {object} config - Meter configuration
|
|
527
533
|
*/
|
|
528
534
|
async updateCosts(type, meterName, config) {
|
|
529
|
-
const basePath =
|
|
535
|
+
const basePath = `${type}.${meterName}`;
|
|
530
536
|
|
|
531
537
|
// Get consumption values
|
|
532
538
|
const daily = (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
|
|
@@ -549,86 +555,60 @@ class MultiMeterManager {
|
|
|
549
555
|
await this.adapter.setStateAsync(`${basePath}.costs.monthly`, calculator.roundToDecimals(monthlyCost, 2), true);
|
|
550
556
|
await this.adapter.setStateAsync(`${basePath}.costs.yearly`, calculator.roundToDecimals(yearlyCost, 2), true);
|
|
551
557
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// Calculate annual fee (prorated)
|
|
558
|
+
// Calculate accumulated costs based on contract start
|
|
555
559
|
const yearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
|
|
556
|
-
let
|
|
560
|
+
let monthsSinceYearStart = 1;
|
|
561
|
+
let basicChargeAccumulated = 0;
|
|
557
562
|
|
|
558
563
|
if (yearStartState && yearStartState.val) {
|
|
559
564
|
// yearStartState.val is a timestamp (number)
|
|
560
565
|
const yearStartDate = new Date(yearStartState.val);
|
|
561
566
|
if (!isNaN(yearStartDate.getTime())) {
|
|
562
567
|
const now = new Date();
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
+
|
|
569
|
+
// Calculate months since contract start (started months, including current)
|
|
570
|
+
monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, now) + 1;
|
|
571
|
+
|
|
572
|
+
// Calculate accumulated basic charge (monthly fee × months)
|
|
573
|
+
basicChargeAccumulated = (config.grundgebuehr || 0) * monthsSinceYearStart;
|
|
568
574
|
}
|
|
569
575
|
}
|
|
570
576
|
|
|
577
|
+
// Jahresgebühr ist ein FIXER Wert pro Jahr (z.B. 60€)
|
|
578
|
+
// NICHT pro-rata nach Monaten/Tagen berechnen!
|
|
579
|
+
const annualFeeAccumulated = config.jahresgebuehr || 0;
|
|
580
|
+
|
|
581
|
+
// Update basicCharge with accumulated value (not just monthly!)
|
|
582
|
+
await this.adapter.setStateAsync(
|
|
583
|
+
`${basePath}.costs.basicCharge`,
|
|
584
|
+
calculator.roundToDecimals(basicChargeAccumulated, 2),
|
|
585
|
+
true,
|
|
586
|
+
);
|
|
587
|
+
|
|
571
588
|
await this.adapter.setStateAsync(
|
|
572
589
|
`${basePath}.costs.annualFee`,
|
|
573
590
|
calculator.roundToDecimals(annualFeeAccumulated, 2),
|
|
574
591
|
true,
|
|
575
592
|
);
|
|
576
593
|
|
|
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;
|
|
594
|
+
// Calculate total yearly costs and balance
|
|
595
|
+
const totalYearlyCost = yearlyCost + basicChargeAccumulated + annualFeeAccumulated;
|
|
586
596
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
`${basePath}.costs.totalYearly`,
|
|
593
|
-
calculator.roundToDecimals(totalYearlyCost, 2),
|
|
594
|
-
true,
|
|
595
|
-
);
|
|
597
|
+
await this.adapter.setStateAsync(
|
|
598
|
+
`${basePath}.costs.totalYearly`,
|
|
599
|
+
calculator.roundToDecimals(totalYearlyCost, 2),
|
|
600
|
+
true,
|
|
601
|
+
);
|
|
596
602
|
|
|
597
|
-
|
|
598
|
-
|
|
603
|
+
const paidTotal = (config.abschlag || 0) * monthsSinceYearStart;
|
|
604
|
+
const balance = totalYearlyCost - paidTotal;
|
|
599
605
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
606
|
+
this.adapter.log.debug(
|
|
607
|
+
`[${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)}`,
|
|
608
|
+
);
|
|
603
609
|
|
|
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
|
-
}
|
|
610
|
+
await this.adapter.setStateAsync(`${basePath}.costs.paidTotal`, calculator.roundToDecimals(paidTotal, 2), true);
|
|
611
|
+
await this.adapter.setStateAsync(`${basePath}.costs.balance`, calculator.roundToDecimals(balance, 2), true);
|
|
632
612
|
}
|
|
633
613
|
|
|
634
614
|
/**
|
|
@@ -652,7 +632,7 @@ class MultiMeterManager {
|
|
|
652
632
|
let totalCostsYearly = 0;
|
|
653
633
|
|
|
654
634
|
for (const meter of meters) {
|
|
655
|
-
const basePath =
|
|
635
|
+
const basePath = `${type}.${meter.name}`;
|
|
656
636
|
|
|
657
637
|
totalDaily += (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
|
|
658
638
|
totalMonthly += (await this.adapter.getStateAsync(`${basePath}.consumption.monthly`))?.val || 0;
|
package/lib/stateManager.js
CHANGED
|
@@ -817,11 +817,12 @@ async function createUtilityStateStructure(adapter, type, _config = {}) {
|
|
|
817
817
|
async function createMeterStructure(adapter, type, meterName, _config = {}) {
|
|
818
818
|
const isGas = type === 'gas';
|
|
819
819
|
|
|
820
|
+
// All meters now include the meter name in labels for consistency
|
|
820
821
|
const labels = {
|
|
821
|
-
gas: { name:
|
|
822
|
-
water: { name:
|
|
823
|
-
electricity: { name:
|
|
824
|
-
pv: { name:
|
|
822
|
+
gas: { name: `Gas (${meterName})`, unit: 'kWh', volumeUnit: 'm³' },
|
|
823
|
+
water: { name: `Wasser (${meterName})`, unit: 'm³', volumeUnit: 'm³' },
|
|
824
|
+
electricity: { name: `Strom (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
|
|
825
|
+
pv: { name: `PV (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
|
|
825
826
|
};
|
|
826
827
|
|
|
827
828
|
const label = labels[type];
|
|
@@ -832,7 +833,7 @@ async function createMeterStructure(adapter, type, meterName, _config = {}) {
|
|
|
832
833
|
// Fallback to prevent crash
|
|
833
834
|
return;
|
|
834
835
|
}
|
|
835
|
-
const basePath =
|
|
836
|
+
const basePath = `${type}.${meterName}`;
|
|
836
837
|
|
|
837
838
|
// Create main channel
|
|
838
839
|
await adapter.setObjectNotExistsAsync(basePath, {
|
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',
|
|
@@ -209,20 +195,15 @@ class UtilityMonitor extends utils.Adapter {
|
|
|
209
195
|
if (id.includes('.billing.closePeriod') && state.val === true && !state.ack) {
|
|
210
196
|
const parts = id.split('.');
|
|
211
197
|
|
|
212
|
-
// Parse state ID: nebenkosten-monitor.0.gas.
|
|
213
|
-
// Remove adapter prefix: gas.
|
|
198
|
+
// Parse state ID: nebenkosten-monitor.0.gas.meterName.billing.closePeriod
|
|
199
|
+
// Remove adapter prefix: gas.meterName.billing.closePeriod
|
|
214
200
|
const statePathParts = parts.slice(2); // Remove "nebenkosten-monitor" and "0"
|
|
215
201
|
|
|
216
|
-
//
|
|
217
|
-
if (statePathParts.length ===
|
|
218
|
-
// Main meter: gas.billing.closePeriod
|
|
219
|
-
const type = statePathParts[0];
|
|
220
|
-
this.log.info(`User triggered billing period closure for ${type} (main meter)`);
|
|
221
|
-
await this.closeBillingPeriod(type);
|
|
222
|
-
} else if (statePathParts.length === 4) {
|
|
223
|
-
// Additional meter: gas.erdgeschoss.billing.closePeriod
|
|
202
|
+
// All meters now use: gas.meterName.billing.closePeriod (length === 4)
|
|
203
|
+
if (statePathParts.length === 4) {
|
|
224
204
|
const type = statePathParts[0];
|
|
225
205
|
const meterName = statePathParts[1];
|
|
206
|
+
|
|
226
207
|
this.log.info(`User triggered billing period closure for ${type}.${meterName}`);
|
|
227
208
|
|
|
228
209
|
// Find the meter object from multiMeterManager
|
|
@@ -235,24 +216,42 @@ class UtilityMonitor extends utils.Adapter {
|
|
|
235
216
|
this.log.error(`Meter "${meterName}" not found for type ${type}!`);
|
|
236
217
|
await this.setStateAsync(`${type}.${meterName}.billing.closePeriod`, false, true);
|
|
237
218
|
}
|
|
219
|
+
} else {
|
|
220
|
+
// Invalid path format
|
|
221
|
+
this.log.warn(`Invalid billing period closure trigger: ${id}`);
|
|
238
222
|
}
|
|
239
223
|
return;
|
|
240
224
|
}
|
|
241
225
|
|
|
242
226
|
// Check if this is an adjustment value change
|
|
227
|
+
// New structure: gas.meterName.adjustment.value (4 parts after namespace)
|
|
243
228
|
if (id.includes('.adjustment.value') && !state.ack) {
|
|
244
229
|
const parts = id.split('.');
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await this.setStateAsync(`${type}.adjustment.applied`, Date.now(), true);
|
|
230
|
+
// Parse: nebenkosten-monitor.0.gas.meterName.adjustment.value
|
|
231
|
+
const statePathParts = parts.slice(2); // Remove "nebenkosten-monitor" and "0"
|
|
248
232
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
233
|
+
if (statePathParts.length === 3) {
|
|
234
|
+
// New structure: gas.meterName.adjustment.value
|
|
235
|
+
const type = statePathParts[0];
|
|
236
|
+
const meterName = statePathParts[1];
|
|
237
|
+
|
|
238
|
+
this.log.info(`Adjustment value changed for ${type}.${meterName}: ${state.val}`);
|
|
239
|
+
await this.setStateAsync(`${type}.${meterName}.adjustment.applied`, Date.now(), true);
|
|
240
|
+
|
|
241
|
+
// Update costs for THIS specific meter
|
|
242
|
+
if (this.multiMeterManager) {
|
|
243
|
+
const meters = this.multiMeterManager.getMetersForType(type);
|
|
244
|
+
const meter = meters.find(m => m.name === meterName);
|
|
245
|
+
if (meter) {
|
|
246
|
+
await this.multiMeterManager.updateCosts(type, meterName, meter.config);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
// Determine which utility this sensor belongs to
|
|
255
|
-
//
|
|
254
|
+
// All meters (including main) are now handled by multiMeterManager
|
|
256
255
|
if (this.multiMeterManager) {
|
|
257
256
|
const meterInfo = this.multiMeterManager.findMeterBySensor(id);
|
|
258
257
|
if (meterInfo && typeof state.val === 'number') {
|
|
@@ -260,19 +259,6 @@ class UtilityMonitor extends utils.Adapter {
|
|
|
260
259
|
return;
|
|
261
260
|
}
|
|
262
261
|
}
|
|
263
|
-
|
|
264
|
-
// Check main meter sensors
|
|
265
|
-
const types = ['gas', 'water', 'electricity', 'pv'];
|
|
266
|
-
for (const type of types) {
|
|
267
|
-
const configType = this.consumptionManager.getConfigType(type);
|
|
268
|
-
|
|
269
|
-
if (this.config[`${configType}Aktiv`] && this.config[`${configType}SensorDP`] === id) {
|
|
270
|
-
if (typeof state.val === 'number') {
|
|
271
|
-
await this.handleSensorUpdate(type, id, state.val);
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
262
|
}
|
|
277
263
|
|
|
278
264
|
/**
|