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.
@@ -155,10 +155,14 @@ class BillingManager {
155
155
  monthsSinceContract = Math.max(1, yDiff * 12 + mDiff + 1);
156
156
  }
157
157
 
158
+ // Calculate accumulated basic charge (monthly fee × months)
158
159
  const basicChargeAccumulated = basicChargeMonthly * monthsSinceContract;
159
- const annualFeeAccumulated = (annualFeePerYear / 12) * monthsSinceContract;
160
- const totalFixCostsAccumulated = basicChargeAccumulated + annualFeeAccumulated;
161
- const totalYearlyCost = yearlyConsumptionCost + totalFixCostsAccumulated;
160
+
161
+ // Jahresgebühr ist ein FIXER Wert pro Jahr (z.B. 60€)
162
+ // NICHT pro-rata nach Monaten/Tagen berechnen!
163
+ const annualFeeAccumulated = annualFeePerYear;
164
+
165
+ const totalYearlyCost = yearlyConsumptionCost + basicChargeAccumulated + annualFeeAccumulated;
162
166
 
163
167
  // Update states
164
168
  await this.adapter.setStateAsync(
@@ -186,9 +190,11 @@ class BillingManager {
186
190
  calculator.roundToDecimals(annualFeeAccumulated, 2),
187
191
  true,
188
192
  );
193
+ // basicCharge enthält NUR die monatliche Grundgebühr (akkumuliert)
194
+ // Jahresgebühr ist separat in annualFee
189
195
  await this.adapter.setStateAsync(
190
196
  `${type}.costs.basicCharge`,
191
- calculator.roundToDecimals(totalFixCostsAccumulated, 2),
197
+ calculator.roundToDecimals(basicChargeAccumulated, 2),
192
198
  true,
193
199
  );
194
200
 
@@ -416,7 +422,7 @@ class BillingManager {
416
422
  * @param {object} meter - Meter object from multiMeterManager
417
423
  */
418
424
  async closeBillingPeriodForMeter(type, meter) {
419
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
425
+ const basePath = `${type}.${meter.name}`;
420
426
  const label = meter.displayName || meter.name;
421
427
 
422
428
  this.adapter.log.info(`🔔 Schließe Abrechnungszeitraum für ${basePath} (${label})...`);
@@ -432,14 +438,8 @@ class BillingManager {
432
438
  return;
433
439
  }
434
440
 
435
- // Get contract date for THIS meter
436
- let contractStartDate;
437
- if (meter.name === 'main') {
438
- const configType = this.adapter.consumptionManager.getConfigType(type);
439
- contractStartDate = this.adapter.config[`${configType}ContractStart`];
440
- } else {
441
- contractStartDate = meter.config?.contractStart;
442
- }
441
+ // Get contract date for THIS meter (all meters have config.contractStart)
442
+ const contractStartDate = meter.config?.contractStart;
443
443
 
444
444
  if (!contractStartDate) {
445
445
  this.adapter.log.error(`❌ Kein Vertragsbeginn für ${basePath} konfiguriert.`);
@@ -491,42 +491,49 @@ class BillingManager {
491
491
  }
492
492
 
493
493
  /**
494
- * Updates billing countdown
494
+ * Updates billing countdown for all meters of a type
495
+ * NOTE: Since v1.4.6, this updates ALL meters (main + additional)
495
496
  *
496
497
  * @param {string} type - Utility type
497
498
  */
498
499
  async updateBillingCountdown(type) {
499
- const configType = this.adapter.consumptionManager.getConfigType(type);
500
- const contractStart = this.adapter.config[`${configType}ContractStart`];
500
+ // Get all meters for this type
501
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
501
502
 
502
- if (!contractStart) {
503
- return;
504
- }
503
+ // Update countdown for each meter based on its contract date
504
+ for (const meter of meters) {
505
+ const contractStart = meter.config?.contractStart;
505
506
 
506
- const startDate = calculator.parseGermanDate(contractStart);
507
- if (!startDate) {
508
- return;
509
- }
507
+ if (!contractStart) {
508
+ continue;
509
+ }
510
510
 
511
- const today = new Date();
512
- const nextAnniversary = new Date(startDate);
513
- nextAnniversary.setFullYear(today.getFullYear());
511
+ const startDate = calculator.parseGermanDate(contractStart);
512
+ if (!startDate) {
513
+ continue;
514
+ }
514
515
 
515
- if (nextAnniversary < today) {
516
- nextAnniversary.setFullYear(today.getFullYear() + 1);
517
- }
516
+ const today = new Date();
517
+ const nextAnniversary = new Date(startDate);
518
+ nextAnniversary.setFullYear(today.getFullYear());
518
519
 
519
- const msPerDay = 1000 * 60 * 60 * 24;
520
- const daysRemaining = Math.ceil((nextAnniversary.getTime() - today.getTime()) / msPerDay);
521
- const displayPeriodEnd = new Date(nextAnniversary);
522
- displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
520
+ if (nextAnniversary < today) {
521
+ nextAnniversary.setFullYear(today.getFullYear() + 1);
522
+ }
523
523
 
524
- await this.adapter.setStateAsync(`${type}.billing.daysRemaining`, daysRemaining, true);
525
- await this.adapter.setStateAsync(
526
- `${type}.billing.periodEnd`,
527
- displayPeriodEnd.toLocaleDateString('de-DE'),
528
- true,
529
- );
524
+ const msPerDay = 1000 * 60 * 60 * 24;
525
+ const daysRemaining = Math.ceil((nextAnniversary.getTime() - today.getTime()) / msPerDay);
526
+ const displayPeriodEnd = new Date(nextAnniversary);
527
+ displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
528
+
529
+ const basePath = `${type}.${meter.name}`;
530
+ await this.adapter.setStateAsync(`${basePath}.billing.daysRemaining`, daysRemaining, true);
531
+ await this.adapter.setStateAsync(
532
+ `${basePath}.billing.periodEnd`,
533
+ displayPeriodEnd.toLocaleDateString('de-DE'),
534
+ true,
535
+ );
536
+ }
530
537
  }
531
538
 
532
539
  /**
@@ -574,21 +581,14 @@ class BillingManager {
574
581
  // YEARLY RESET: Each meter resets individually based on ITS contract date
575
582
  const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
576
583
  for (const meter of meters) {
577
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
584
+ const basePath = `${type}.${meter.name}`;
578
585
  const lastYearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
579
586
 
580
587
  if (lastYearStartState?.val) {
581
588
  const lastYearStartDate = new Date(lastYearStartState.val);
582
589
 
583
- // Get contract date for THIS specific meter
584
- let contractStartDate;
585
- if (meter.name === 'main') {
586
- // Main meter: use adapter config
587
- contractStartDate = this.adapter.config[`${configType}ContractStart`];
588
- } else {
589
- // Additional meter: use meter's individual config
590
- contractStartDate = meter.config?.contractStart;
591
- }
590
+ // Get contract date for THIS specific meter (all meters have config.contractStart)
591
+ const contractStartDate = meter.config?.contractStart;
592
592
 
593
593
  if (contractStartDate) {
594
594
  const contractStart = calculator.parseGermanDate(contractStartDate);
@@ -644,7 +644,7 @@ class BillingManager {
644
644
 
645
645
  // Reset each meter
646
646
  for (const meter of meters) {
647
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
647
+ const basePath = `${type}.${meter.name}`;
648
648
  const label = meter.displayName || meter.name;
649
649
 
650
650
  this.adapter.log.debug(`Resetting daily counter for ${basePath} (${label})`);
@@ -713,7 +713,7 @@ class BillingManager {
713
713
 
714
714
  // Reset each meter
715
715
  for (const meter of meters) {
716
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
716
+ const basePath = `${type}.${meter.name}`;
717
717
  const label = meter.displayName || meter.name;
718
718
 
719
719
  this.adapter.log.debug(`Resetting monthly counter for ${basePath} (${label})`);
@@ -771,7 +771,7 @@ class BillingManager {
771
771
 
772
772
  // Reset each meter
773
773
  for (const meter of meters) {
774
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
774
+ const basePath = `${type}.${meter.name}`;
775
775
  const label = meter.displayName || meter.name;
776
776
 
777
777
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
@@ -803,7 +803,7 @@ class BillingManager {
803
803
  * @param {object} meter - Meter object from multiMeterManager
804
804
  */
805
805
  async resetYearlyCountersForMeter(type, meter) {
806
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
806
+ const basePath = `${type}.${meter.name}`;
807
807
  const label = meter.displayName || meter.name;
808
808
 
809
809
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
@@ -47,8 +47,8 @@ class ConsumptionManager {
47
47
 
48
48
  this.adapter.log.info(`Initializing ${type} monitoring...`);
49
49
 
50
- // Create state structure
51
- await stateManager.createUtilityStateStructure(this.adapter, type, this.adapter.config);
50
+ // State structure is now created by MultiMeterManager per meter (v1.4.6)
51
+ // Old createUtilityStateStructure removed - states are created under type.meterName.*
52
52
 
53
53
  const configType = this.getConfigType(type);
54
54
  const sensorDPKey = `${configType}SensorDP`;
@@ -56,7 +56,7 @@ class ConsumptionManager {
56
56
 
57
57
  if (!sensorDP) {
58
58
  this.adapter.log.warn(`${type} is active but no sensor datapoint configured!`);
59
- await this.adapter.setStateAsync(`${type}.info.sensorActive`, false, true);
59
+ // Note: sensorActive state is now created per meter by MultiMeterManager
60
60
  return;
61
61
  }
62
62
 
@@ -69,91 +69,34 @@ class ConsumptionManager {
69
69
  this.adapter.log.info(`${type}: Managed with contract start: ${contractStartDateStr}`);
70
70
  }
71
71
 
72
- // Subscribe to sensor datapoint
73
- this.adapter.subscribeForeignStates(sensorDP);
74
- await this.adapter.setStateAsync(`${type}.info.sensorActive`, true, true);
75
- this.adapter.log.debug(`Subscribed to ${type} sensor: ${sensorDP}`);
72
+ // Sensor subscription is now handled by MultiMeterManager per meter
73
+ this.adapter.log.debug(`${type} sensor will be subscribed by MultiMeterManager: ${sensorDP}`);
76
74
 
77
75
  // Initialize all meters (main + additional) via MultiMeterManager
76
+ // This handles everything now: state creation, sensor subscription, costs calculation
78
77
  if (this.adapter.multiMeterManager) {
79
78
  await this.adapter.multiMeterManager.initializeType(type);
80
79
  }
81
80
 
82
- // Restore last sensor value from persistent state to prevent delta loss
83
- const lastReading = await this.adapter.getStateAsync(`${type}.info.meterReading`);
84
- if (lastReading && typeof lastReading.val === 'number') {
85
- this.lastSensorValues[sensorDP] = lastReading.val;
86
- this.adapter.log.debug(`${type}: Restored last sensor value: ${lastReading.val}`);
87
- }
88
-
89
- // Initialize with current sensor value
90
- try {
91
- const sensorState = await this.adapter.getForeignStateAsync(sensorDP);
92
- if (sensorState && sensorState.val !== null && typeof sensorState.val === 'number') {
93
- await this.handleSensorUpdate(type, sensorDP, sensorState.val);
94
- }
95
- } catch (error) {
96
- this.adapter.log.warn(`Could not read initial value from ${sensorDP}: ${error.message}`);
97
- }
98
-
99
- // Initialize period start timestamps if not set
100
- const now = Date.now();
101
- const dayStart = await this.adapter.getStateAsync(`${type}.statistics.lastDayStart`);
102
- if (!dayStart || !dayStart.val) {
103
- await this.adapter.setStateAsync(`${type}.statistics.lastDayStart`, now, true);
104
- }
105
-
106
- const monthStart = await this.adapter.getStateAsync(`${type}.statistics.lastMonthStart`);
107
- if (!monthStart || !monthStart.val) {
108
- await this.adapter.setStateAsync(`${type}.statistics.lastMonthStart`, now, true);
109
- }
110
-
111
- const yearStart = await this.adapter.getStateAsync(`${type}.statistics.lastYearStart`);
112
- if (!yearStart || !yearStart.val) {
113
- // Determine year start based on contract date or January 1st
114
- const contractStartKey = `${configType}ContractStart`;
115
- const contractStartDateStr = this.adapter.config[contractStartKey];
116
-
117
- let yearStartDate;
118
- if (contractStartDateStr) {
119
- const contractStart = calculator.parseGermanDate(contractStartDateStr);
120
- if (contractStart && !isNaN(contractStart.getTime())) {
121
- // Calculate last anniversary
122
- const nowDate = new Date(now);
123
- const currentYear = nowDate.getFullYear();
124
- yearStartDate = new Date(currentYear, contractStart.getMonth(), contractStart.getDate(), 12, 0, 0);
125
-
126
- // If anniversary is in the future this year, take last year
127
- if (yearStartDate > nowDate) {
128
- yearStartDate.setFullYear(currentYear - 1);
129
- }
130
- }
131
- }
132
-
133
- if (!yearStartDate) {
134
- // Fallback: January 1st of current year
135
- const nowDate = new Date(now);
136
- yearStartDate = new Date(nowDate.getFullYear(), 0, 1, 12, 0, 0);
137
- this.adapter.log.info(
138
- `${type}: No contract start found. Setting initial year start to January 1st: ${yearStartDate.toLocaleDateString('de-DE')}`,
139
- );
140
- }
141
-
142
- await this.adapter.setStateAsync(`${type}.statistics.lastYearStart`, yearStartDate.getTime(), true);
143
- }
144
- // Update current price
145
- await this.updateCurrentPrice(type);
146
-
147
- // Initial cost calculation (wichtig! Sonst bleiben Kosten bei 0)
148
- if (typeof this.adapter.updateCosts === 'function') {
149
- await this.adapter.updateCosts(type);
150
- }
81
+ // Note: All initialization moved to MultiMeterManager in v1.4.6:
82
+ // - Sensor value restoration (per meter)
83
+ // - Period start timestamps (per meter)
84
+ // - Current price updates (per meter)
85
+ // - Cost calculations (per meter)
86
+ // Old type-level states (gas.info.*, gas.statistics.*) are no longer used
151
87
 
152
88
  // Initialize yearly consumption from initial reading if set
89
+ // NOTE: This is now handled per meter by MultiMeterManager in v1.4.6
90
+ // This legacy code path should not execute for new setups, but is kept for safety
153
91
  const initialReadingKey = `${configType}InitialReading`;
154
92
  const initialReading = this.adapter.config[initialReadingKey] || 0;
155
93
 
156
- if (initialReading > 0) {
94
+ if (initialReading > 0 && sensorDP) {
95
+ // Get the main meter name to use the correct path
96
+ const mainMeterNameKey = `${configType}MainMeterName`;
97
+ const mainMeterName = this.adapter.config[mainMeterNameKey] || 'main';
98
+ const basePath = `${type}.${mainMeterName}`;
99
+
157
100
  const sensorState = await this.adapter.getForeignStateAsync(sensorDP);
158
101
  if (sensorState && typeof sensorState.val === 'number') {
159
102
  let currentRaw = sensorState.val;
@@ -173,7 +116,7 @@ class ConsumptionManager {
173
116
  const zZahl = this.adapter.config.gasZahl || 0.95;
174
117
  const yearlyVolume = yearlyConsumption;
175
118
  yearlyConsumption = calculator.convertGasM3ToKWh(yearlyConsumption, brennwert, zZahl);
176
- await this.adapter.setStateAsync(`${type}.consumption.yearlyVolume`, yearlyVolume, true);
119
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, yearlyVolume, true);
177
120
  this.adapter.log.info(
178
121
  `Init yearly ${type}: ${yearlyConsumption.toFixed(2)} kWh = ${(currentRaw - initialReading).toFixed(2)} m³ (current: ${currentRaw.toFixed(2)} m³, initial: ${initialReading} m³)`,
179
122
  );
@@ -183,17 +126,15 @@ class ConsumptionManager {
183
126
  );
184
127
  }
185
128
 
186
- await this.adapter.setStateAsync(`${type}.consumption.yearly`, yearlyConsumption, true);
129
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyConsumption, true);
187
130
  if (typeof this.adapter.updateCosts === 'function') {
188
131
  await this.adapter.updateCosts(type);
189
132
  }
190
133
  }
191
134
  }
192
135
 
193
- // Update billing countdown
194
- if (typeof this.adapter.updateBillingCountdown === 'function') {
195
- await this.adapter.updateBillingCountdown(type);
196
- }
136
+ // Note: Billing countdown is now handled per meter by billingManager.updateBillingCountdown()
137
+ // which is called during checkPeriodResets()
197
138
 
198
139
  this.adapter.log.debug(`Initial cost calculation completed for ${type}`);
199
140
  }
@@ -201,17 +142,25 @@ class ConsumptionManager {
201
142
  /**
202
143
  * Handles sensor value updates
203
144
  *
145
+ * NOTE: This method is DEPRECATED since v1.4.6!
146
+ * All sensor updates are now handled by MultiMeterManager.handleSensorUpdate()
147
+ * This method remains only as fallback but should NEVER be called in normal operation.
148
+ *
204
149
  * @param {string} type - Utility type
205
150
  * @param {string} sensorDP - Sensor datapoint ID
206
151
  * @param {number} value - New sensor value
207
152
  */
208
153
  async handleSensorUpdate(type, sensorDP, value) {
154
+ this.adapter.log.warn(
155
+ `consumptionManager.handleSensorUpdate() called - this is deprecated! All sensors should be handled by MultiMeterManager.`,
156
+ );
157
+
209
158
  if (typeof value !== 'number' || value < 0) {
210
159
  this.adapter.log.warn(`Invalid sensor value for ${type}: ${value}`);
211
160
  return;
212
161
  }
213
162
 
214
- this.adapter.log.debug(`Sensor update for ${type}: ${value}`);
163
+ this.adapter.log.debug(`[DEPRECATED] Sensor update for ${type}: ${value}`);
215
164
 
216
165
  const now = Date.now();
217
166
  let consumption = value;
@@ -371,36 +320,47 @@ class ConsumptionManager {
371
320
  }
372
321
 
373
322
  /**
374
- * Updates the current price display
323
+ * Updates the current price display for all meters of a type
324
+ * NOTE: Since v1.4.6, this updates ALL meters (main + additional)
325
+ * Only main meters support HT/NT, additional meters have fixed price
375
326
  *
376
327
  * @param {string} type - Utility type
377
328
  */
378
329
  async updateCurrentPrice(type) {
379
330
  const configType = this.getConfigType(type);
380
331
 
381
- // Check for HT/NT
382
- const htNtEnabledKey = `${configType}HtNtEnabled`;
383
- const htNtEnabled = this.adapter.config[htNtEnabledKey] || false;
332
+ // Get all meters for this type
333
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
384
334
 
385
- let tariffName = 'Standard';
386
- let activePrice = 0;
335
+ for (const meter of meters) {
336
+ let tariffName = 'Standard';
337
+ let activePrice = 0;
387
338
 
388
- if (htNtEnabled) {
389
- const isHT = calculator.isHTTime(this.adapter.config, configType);
390
- if (isHT) {
391
- activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
392
- tariffName = 'Haupttarif (HT)';
339
+ // Check if this meter has HT/NT enabled
340
+ const htNtEnabled = meter.config?.htNtEnabled || false;
341
+
342
+ if (htNtEnabled) {
343
+ const isHT = calculator.isHTTime(this.adapter.config, configType);
344
+ if (isHT) {
345
+ activePrice = this.adapter.config[`${configType}HtPrice`] || 0;
346
+ tariffName = 'Haupttarif (HT)';
347
+ } else {
348
+ activePrice = this.adapter.config[`${configType}NtPrice`] || 0;
349
+ tariffName = 'Nebentarif (NT)';
350
+ }
393
351
  } else {
394
- activePrice = this.adapter.config[`${configType}NtPrice`] || 0;
395
- tariffName = 'Nebentarif (NT)';
352
+ // Use meter's configured price
353
+ activePrice = meter.config?.preis || 0;
396
354
  }
397
- } else {
398
- const priceKey = `${configType}Preis`;
399
- activePrice = this.adapter.config[priceKey] || 0;
400
- }
401
355
 
402
- await this.adapter.setStateAsync(`${type}.info.currentPrice`, calculator.roundToDecimals(activePrice, 4), true);
403
- await this.adapter.setStateAsync(`${type}.info.currentTariff`, tariffName, true);
356
+ const basePath = `${type}.${meter.name}`;
357
+ await this.adapter.setStateAsync(
358
+ `${basePath}.info.currentPrice`,
359
+ calculator.roundToDecimals(activePrice, 4),
360
+ true,
361
+ );
362
+ await this.adapter.setStateAsync(`${basePath}.info.currentTariff`, tariffName, true);
363
+ }
404
364
  }
405
365
  }
406
366