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.
@@ -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 === 'getInstances') {
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 current days remaining
229
- const daysRemainingState = await this.adapter.getStateAsync(`${type}.billing.daysRemaining`);
230
- const daysRemaining = typeof daysRemainingState?.val === 'number' ? daysRemainingState.val : 999;
231
- const periodEndState = await this.adapter.getStateAsync(`${type}.billing.periodEnd`);
232
- const periodEnd = periodEndState?.val || '--.--.----';
233
-
234
- // 1. BILLING END REMINDER (Zählerstand ablesen)
235
- if (this.adapter.config.notificationBillingEnabled) {
236
- const billingSent = await this.adapter.getStateAsync(`${type}.billing.notificationSent`);
237
- const billingDaysThreshold = this.adapter.config.notificationBillingDays || 7;
238
-
239
- if (billingSent?.val !== true && daysRemaining <= billingDaysThreshold) {
240
- const message =
241
- `🔔 *Nebenkosten-Monitor: Zählerstand ablesen*\n\n` +
242
- `Dein Abrechnungszeitraum für *${typesDe[type]}* endet in ${daysRemaining} Tagen!\n\n` +
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
- // 2. CONTRACT CHANGE REMINDER (Tarif wechseln / Kündigungsfrist)
253
- if (this.adapter.config.notificationChangeEnabled) {
254
- const changeSent = await this.adapter.getStateAsync(`${type}.billing.notificationChangeSent`);
255
- const changeDaysThreshold = this.adapter.config.notificationChangeDays || 60;
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
- if (changeSent?.val !== true && daysRemaining <= changeDaysThreshold) {
258
- const message =
259
- `💡 *Nebenkosten-Monitor: Tarif-Check*\n\n` +
260
- `Dein Vertrag für *${typesDe[type]}* endet am ${periodEnd}.\n\n` +
261
- `⏰ Noch ${daysRemaining} Tage bis zum Ende des Zeitraums.\n\n` +
262
- `Jetzt ist ein guter Zeitpunkt, um Preise zu vergleichen oder die Kündigungsfrist zu prüfen! 💸`;
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
- await this.sendNotification(type, message, 'change');
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 main meter as representative
324
- paidTotalState = await this.adapter.getStateAsync(`${type}.costs.paidTotal`);
325
- balanceState = await this.adapter.getStateAsync(`${type}.costs.balance`);
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
- // Single meter: use main meter values
329
- yearlyState = await this.adapter.getStateAsync(`${type}.consumption.yearly`);
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} type - gas, water, electricity
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(type, message, reminderType) {
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 ${type} via ${instance}`);
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(`${type}.billing.${stateKey}`, true, true);
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 ${type}: ${error.message}`);
395
+ this.adapter.log.error(`Failed to send ${reminderType} notification for ${pathOrType}: ${error.message}`);
397
396
  }
398
397
  }
399
398
  }
@@ -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: 'main',
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 = meterName === 'main' ? type : `${type}.${meterName}`;
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 = meterName === 'main' ? type : `${type}.${meterName}`;
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 = meterName === 'main' ? type : `${type}.${meterName}`;
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 main meter supports HT/NT
501
- if (meterName === 'main' && config.htNtEnabled) {
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 = meterName === 'main' ? type : `${type}.${meterName}`;
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
- await this.adapter.setStateAsync(`${basePath}.costs.basicCharge`, Number(config.grundgebuehr) || 0, true);
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 annualFeeAccumulated = 0;
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
- const yearStartTime = yearStartDate.getTime();
564
- const nowTime = now.getTime();
565
- const daysSinceYearStart = Math.floor((nowTime - yearStartTime) / (1000 * 60 * 60 * 24));
566
- const daysInYear = calculator.isLeapYear(now.getFullYear()) ? 366 : 365;
567
- annualFeeAccumulated = ((config.jahresgebuehr || 0) / daysInYear) * daysSinceYearStart;
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 balance and total yearly costs
578
- if (yearStartState && yearStartState.val) {
579
- // yearStartState.val is a timestamp (number)
580
- const yearStartDate = new Date(yearStartState.val);
581
- if (!isNaN(yearStartDate.getTime())) {
582
- const now = new Date();
583
- // Calculate paid total based on started months (not just completed months)
584
- // If current month has started, count it as paid
585
- const monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, now) + 1;
594
+ // Calculate total yearly costs and balance
595
+ const totalYearlyCost = yearlyCost + basicChargeAccumulated + annualFeeAccumulated;
586
596
 
587
- // Calculate total yearly costs with correct months
588
- const basicChargeAccumulated = (config.grundgebuehr || 0) * monthsSinceYearStart;
589
- const totalYearlyCost = yearlyCost + basicChargeAccumulated + annualFeeAccumulated;
590
-
591
- await this.adapter.setStateAsync(
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
- const paidTotal = (config.abschlag || 0) * monthsSinceYearStart;
598
- const balance = paidTotal - totalYearlyCost;
603
+ const paidTotal = (config.abschlag || 0) * monthsSinceYearStart;
604
+ const balance = totalYearlyCost - paidTotal;
599
605
 
600
- this.adapter.log.debug(
601
- `[${basePath}] Balance calculation: abschlag=${config.abschlag}, months=${monthsSinceYearStart}, paidTotal=${paidTotal.toFixed(2)}, totalYearly=${totalYearlyCost.toFixed(2)}, balance=${balance.toFixed(2)}`,
602
- );
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
- await this.adapter.setStateAsync(
605
- `${basePath}.costs.paidTotal`,
606
- calculator.roundToDecimals(paidTotal, 2),
607
- true,
608
- );
609
- await this.adapter.setStateAsync(
610
- `${basePath}.costs.balance`,
611
- calculator.roundToDecimals(balance, 2),
612
- true,
613
- );
614
- } else {
615
- // Fallback if yearStartDate parsing fails
616
- const totalYearlyCost = yearlyCost + annualFeeAccumulated;
617
- await this.adapter.setStateAsync(
618
- `${basePath}.costs.totalYearly`,
619
- calculator.roundToDecimals(totalYearlyCost, 2),
620
- true,
621
- );
622
- }
623
- } else {
624
- // Fallback if no yearStartState exists
625
- const totalYearlyCost = yearlyCost + annualFeeAccumulated;
626
- await this.adapter.setStateAsync(
627
- `${basePath}.costs.totalYearly`,
628
- calculator.roundToDecimals(totalYearlyCost, 2),
629
- true,
630
- );
631
- }
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 = meter.name === 'main' ? type : `${type}.${meter.name}`;
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;
@@ -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: meterName === 'main' ? 'Gas' : `Gas (${meterName})`, unit: 'kWh', volumeUnit: 'm³' },
822
- water: { name: meterName === 'main' ? 'Wasser' : `Wasser (${meterName})`, unit: 'm³', volumeUnit: 'm³' },
823
- electricity: { name: meterName === 'main' ? 'Strom' : `Strom (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
824
- pv: { name: meterName === 'main' ? 'PV' : `PV (${meterName})`, unit: 'kWh', volumeUnit: 'kWh' },
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 = meterName === 'main' ? type : `${type}.${meterName}`;
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.erdgeschoss.billing.closePeriod
213
- // Remove adapter prefix: gas.erdgeschoss.billing.closePeriod
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
- // Determine if this is main meter or additional meter
217
- if (statePathParts.length === 3) {
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
- const type = parts[parts.length - 3];
246
- this.log.info(`Adjustment value changed for ${type}: ${state.val}`);
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
- // Update costs for all meters of this type
250
- await this.updateCosts(type);
251
- return;
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
- // First check if it's a multi-meter sensor (additional meters)
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
  /**