iobroker.utility-monitor 1.4.5 → 1.5.0

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.
Files changed (35) hide show
  1. package/README.md +159 -44
  2. package/admin/custom/.vite/manifest.json +90 -0
  3. package/admin/custom/@mf-types/Components.d.ts +2 -0
  4. package/admin/custom/@mf-types/compiled-types/Components/CSVImporter.d.ts +11 -0
  5. package/admin/custom/@mf-types/compiled-types/Components.d.ts +2 -0
  6. package/admin/custom/@mf-types.d.ts +3 -0
  7. package/admin/custom/@mf-types.zip +0 -0
  8. package/admin/custom/CSVImporter_v15_11.js +4415 -0
  9. package/admin/custom/assets/Components-i0AZ59nl.js +18887 -0
  10. package/admin/custom/assets/UtilityMonitor__loadShare__react__loadShare__-Da99Mak4.js +42 -0
  11. package/admin/custom/assets/UtilityMonitor__mf_v__runtimeInit__mf_v__-BmC4OGk6.js +16 -0
  12. package/admin/custom/assets/_commonjsHelpers-Dj2_voLF.js +30 -0
  13. package/admin/custom/assets/hostInit-DEXfeB0W.js +10 -0
  14. package/admin/custom/assets/index-B3WVNJTz.js +401 -0
  15. package/admin/custom/assets/index-VBwl8x_k.js +64 -0
  16. package/admin/custom/assets/preload-helper-BelkbqnE.js +61 -0
  17. package/admin/custom/assets/virtualExposes-CqCLUNLT.js +19 -0
  18. package/admin/custom/index.html +12 -0
  19. package/admin/custom/mf-manifest.json +1 -0
  20. package/admin/jsonConfig.json +219 -35
  21. package/io-package.json +51 -2
  22. package/lib/billingManager.js +276 -170
  23. package/lib/calculator.js +19 -138
  24. package/lib/consumptionManager.js +48 -331
  25. package/lib/importManager.js +300 -0
  26. package/lib/messagingHandler.js +112 -49
  27. package/lib/meter/MeterRegistry.js +110 -0
  28. package/lib/multiMeterManager.js +410 -181
  29. package/lib/stateManager.js +508 -36
  30. package/lib/utils/billingHelper.js +69 -0
  31. package/lib/utils/consumptionHelper.js +47 -0
  32. package/lib/utils/helpers.js +178 -0
  33. package/lib/utils/typeMapper.js +19 -0
  34. package/main.js +99 -36
  35. package/package.json +10 -4
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const calculator = require('./calculator');
4
+ const { getConfigType } = require('./utils/typeMapper');
5
+ const billingHelper = require('./utils/billingHelper');
4
6
 
5
7
  /**
6
8
  * BillingManager handles all cost calculations,
@@ -20,7 +22,7 @@ class BillingManager {
20
22
  * @param {string} type - Utility type
21
23
  */
22
24
  async updateCosts(type) {
23
- const configType = this.adapter.consumptionManager.getConfigType(type);
25
+ const configType = getConfigType(type);
24
26
 
25
27
  // Get price and basic charge from config
26
28
  const priceKey = `${configType}Preis`;
@@ -56,7 +58,7 @@ class BillingManager {
56
58
  const yearlyVolume = typeof yearlyVolumeState?.val === 'number' ? yearlyVolumeState.val : 0;
57
59
  const totalM3 = yearlyVolume + adjustment;
58
60
  const brennwert = this.adapter.config.gasBrennwert || 11.5;
59
- const zZahl = this.adapter.config.gasZahl || 0.95;
61
+ const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
60
62
  yearly = calculator.convertGasM3ToKWh(totalM3, brennwert, zZahl);
61
63
  } else {
62
64
  yearly += adjustment;
@@ -67,7 +69,6 @@ class BillingManager {
67
69
  let dailyConsumptionCost, monthlyConsumptionCost, yearlyConsumptionCost;
68
70
 
69
71
  if (htNtEnabled) {
70
- // HT/NT Calculation
71
72
  const htPrice = this.adapter.config[`${configType}HtPrice`] || 0;
72
73
  const ntPrice = this.adapter.config[`${configType}NtPrice`] || 0;
73
74
 
@@ -82,48 +83,27 @@ class BillingManager {
82
83
  if (adjustment !== 0) {
83
84
  if (type === 'gas') {
84
85
  const brennwert = this.adapter.config.gasBrennwert || 11.5;
85
- const zZahl = this.adapter.config.gasZahl || 0.95;
86
+ const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
86
87
  yearlyHT = Number(yearlyHT) + calculator.convertGasM3ToKWh(adjustment, brennwert, zZahl);
87
88
  } else {
88
89
  yearlyHT = Number(yearlyHT) + Number(adjustment);
89
90
  }
90
91
  }
91
92
 
92
- dailyConsumptionCost = Number(dailyHT) * parseFloat(htPrice) + Number(dailyNT) * parseFloat(ntPrice);
93
- monthlyConsumptionCost = Number(monthlyHT) * parseFloat(htPrice) + Number(monthlyNT) * parseFloat(ntPrice);
94
- yearlyConsumptionCost = Number(yearlyHT) * parseFloat(htPrice) + Number(yearlyNT) * parseFloat(ntPrice);
93
+ const dailyRes = billingHelper.calculateHTNTCosts(dailyHT, htPrice, dailyNT, ntPrice);
94
+ const monthlyRes = billingHelper.calculateHTNTCosts(monthlyHT, htPrice, monthlyNT, ntPrice);
95
+ const yearlyRes = billingHelper.calculateHTNTCosts(yearlyHT, htPrice, yearlyNT, ntPrice);
95
96
 
96
- // Update HT/NT specific cost states
97
- await this.adapter.setStateAsync(
98
- `${type}.costs.dailyHT`,
99
- calculator.roundToDecimals(Number(dailyHT) * parseFloat(htPrice), 2),
100
- true,
101
- );
102
- await this.adapter.setStateAsync(
103
- `${type}.costs.dailyNT`,
104
- calculator.roundToDecimals(Number(dailyNT) * calculator.ensureNumber(ntPrice), 2),
105
- true,
106
- );
107
- await this.adapter.setStateAsync(
108
- `${type}.costs.monthlyHT`,
109
- calculator.roundToDecimals(Number(monthlyHT) * calculator.ensureNumber(htPrice), 2),
110
- true,
111
- );
112
- await this.adapter.setStateAsync(
113
- `${type}.costs.monthlyNT`,
114
- calculator.roundToDecimals(Number(monthlyNT) * calculator.ensureNumber(ntPrice), 2),
115
- true,
116
- );
117
- await this.adapter.setStateAsync(
118
- `${type}.costs.yearlyHT`,
119
- calculator.roundToDecimals(Number(yearlyHT) * calculator.ensureNumber(htPrice), 2),
120
- true,
121
- );
122
- await this.adapter.setStateAsync(
123
- `${type}.costs.yearlyNT`,
124
- calculator.roundToDecimals(Number(yearlyNT) * calculator.ensureNumber(ntPrice), 2),
125
- true,
126
- );
97
+ dailyConsumptionCost = dailyRes.total;
98
+ monthlyConsumptionCost = monthlyRes.total;
99
+ yearlyConsumptionCost = yearlyRes.total;
100
+
101
+ await this.adapter.setStateAsync(`${type}.costs.dailyHT`, dailyRes.htCosts, true);
102
+ await this.adapter.setStateAsync(`${type}.costs.dailyNT`, dailyRes.ntCosts, true);
103
+ await this.adapter.setStateAsync(`${type}.costs.monthlyHT`, monthlyRes.htCosts, true);
104
+ await this.adapter.setStateAsync(`${type}.costs.monthlyNT`, monthlyRes.ntCosts, true);
105
+ await this.adapter.setStateAsync(`${type}.costs.yearlyHT`, yearlyRes.htCosts, true);
106
+ await this.adapter.setStateAsync(`${type}.costs.yearlyNT`, yearlyRes.ntCosts, true);
127
107
  } else {
128
108
  dailyConsumptionCost = calculator.calculateCost(daily, price);
129
109
  monthlyConsumptionCost = calculator.calculateCost(monthly, price);
@@ -131,40 +111,18 @@ class BillingManager {
131
111
  }
132
112
 
133
113
  // Basic charge calculation
134
- const contractStartKey = `${configType}ContractStart`;
135
- const contractStartDate = this.adapter.config[contractStartKey];
136
-
137
- let monthsSinceContract;
138
- if (contractStartDate) {
139
- const contractStart = calculator.parseGermanDate(contractStartDate);
140
- if (contractStart && !isNaN(contractStart.getTime())) {
141
- const now = new Date();
142
- const yDiff = now.getFullYear() - contractStart.getFullYear();
143
- const mDiff = now.getMonth() - contractStart.getMonth();
144
- monthsSinceContract = Math.max(1, yDiff * 12 + mDiff + 1);
145
- }
146
- }
147
-
148
- if (monthsSinceContract === undefined) {
149
- const yearStartState = await this.adapter.getStateAsync(`${type}.statistics.lastYearStart`);
150
- const yearStartTime = typeof yearStartState?.val === 'number' ? yearStartState.val : Date.now();
151
- const yearStart = new Date(yearStartTime);
152
- const now = new Date();
153
- const yDiff = now.getFullYear() - yearStart.getFullYear();
154
- const mDiff = now.getMonth() - yearStart.getMonth();
155
- monthsSinceContract = Math.max(1, yDiff * 12 + mDiff + 1);
156
- }
114
+ const monthsSinceContract = await this._calculateMonthsSinceStart(type, configType);
157
115
 
158
- // Calculate accumulated basic charge (monthly fee × months)
159
- const basicChargeAccumulated = basicChargeMonthly * monthsSinceContract;
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;
116
+ const charges = billingHelper.calculateAccumulatedCharges(
117
+ basicChargeMonthly,
118
+ annualFeePerYear,
119
+ monthsSinceContract,
120
+ );
121
+ const basicChargeAccumulated = charges.basicCharge;
122
+ const annualFeeAccumulated = charges.annualFee;
123
+ const totalYearlyCost = yearlyConsumptionCost + charges.total;
166
124
 
167
- // Update states
125
+ // Update basic charge states
168
126
  await this.adapter.setStateAsync(
169
127
  `${type}.costs.daily`,
170
128
  calculator.roundToDecimals(dailyConsumptionCost, 2),
@@ -185,32 +143,39 @@ class BillingManager {
185
143
  calculator.roundToDecimals(totalYearlyCost, 2),
186
144
  true,
187
145
  );
188
- await this.adapter.setStateAsync(
189
- `${type}.costs.annualFee`,
190
- calculator.roundToDecimals(annualFeeAccumulated, 2),
191
- true,
192
- );
193
- // basicCharge enthält NUR die monatliche Grundgebühr (akkumuliert)
194
- // Jahresgebühr ist separat in annualFee
195
- await this.adapter.setStateAsync(
196
- `${type}.costs.basicCharge`,
197
- calculator.roundToDecimals(basicChargeAccumulated, 2),
198
- true,
199
- );
146
+ await this.adapter.setStateAsync(`${type}.costs.annualFee`, annualFeeAccumulated, true);
147
+ await this.adapter.setStateAsync(`${type}.costs.basicCharge`, basicChargeAccumulated, true);
200
148
 
201
- // Abschlag
149
+ // Abschlag / Installment
202
150
  const abschlagKey = `${configType}Abschlag`;
203
151
  const monthlyAbschlag = this.adapter.config[abschlagKey] || 0;
204
152
 
205
- if (monthlyAbschlag > 0) {
206
- const paidTotal = monthlyAbschlag * monthsSinceContract;
207
- const balance = totalYearlyCost - paidTotal;
208
- await this.adapter.setStateAsync(`${type}.costs.paidTotal`, calculator.roundToDecimals(paidTotal, 2), true);
209
- await this.adapter.setStateAsync(`${type}.costs.balance`, calculator.roundToDecimals(balance, 2), true);
210
- } else {
211
- await this.adapter.setStateAsync(`${type}.costs.paidTotal`, 0, true);
212
- await this.adapter.setStateAsync(`${type}.costs.balance`, 0, true);
153
+ const balanceRes = billingHelper.calculateBalance(monthlyAbschlag, monthsSinceContract, totalYearlyCost);
154
+ await this.adapter.setStateAsync(`${type}.costs.paidTotal`, balanceRes.paid, true);
155
+ await this.adapter.setStateAsync(`${type}.costs.balance`, balanceRes.balance, true);
156
+ }
157
+
158
+ /**
159
+ * Calculates months since contract or year start
160
+ *
161
+ * @param {string} type - Utility type
162
+ * @param {string} configType - Mapped config type
163
+ * @returns {Promise<number>} Number of months (at least 1)
164
+ */
165
+ async _calculateMonthsSinceStart(type, configType) {
166
+ const contractStartDate = this.adapter.config[`${configType}ContractStart`];
167
+ let startDate;
168
+
169
+ if (contractStartDate) {
170
+ startDate = calculator.parseGermanDate(contractStartDate);
171
+ }
172
+
173
+ if (!startDate || isNaN(startDate.getTime())) {
174
+ const lastYearStart = await this.adapter.getStateAsync(`${type}.statistics.lastYearStart`);
175
+ startDate = new Date(lastYearStart?.val || Date.now());
213
176
  }
177
+
178
+ return Math.max(1, calculator.getMonthsDifference(startDate, new Date()) + 1);
214
179
  }
215
180
 
216
181
  /**
@@ -232,7 +197,7 @@ class BillingManager {
232
197
  return;
233
198
  }
234
199
 
235
- const configType = this.adapter.consumptionManager.getConfigType(type);
200
+ const configType = getConfigType(type);
236
201
  const contractStartKey = `${configType}ContractStart`;
237
202
  const contractStart = this.adapter.config[contractStartKey];
238
203
 
@@ -422,7 +387,7 @@ class BillingManager {
422
387
  * @param {object} meter - Meter object from multiMeterManager
423
388
  */
424
389
  async closeBillingPeriodForMeter(type, meter) {
425
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
390
+ const basePath = `${type}.${meter.name}`;
426
391
  const label = meter.displayName || meter.name;
427
392
 
428
393
  this.adapter.log.info(`🔔 Schließe Abrechnungszeitraum für ${basePath} (${label})...`);
@@ -438,14 +403,8 @@ class BillingManager {
438
403
  return;
439
404
  }
440
405
 
441
- // Get contract date for THIS meter
442
- let contractStartDate;
443
- if (meter.name === 'main') {
444
- const configType = this.adapter.consumptionManager.getConfigType(type);
445
- contractStartDate = this.adapter.config[`${configType}ContractStart`];
446
- } else {
447
- contractStartDate = meter.config?.contractStart;
448
- }
406
+ // Get contract date for THIS meter (all meters have config.contractStart)
407
+ const contractStartDate = meter.config?.contractStart;
449
408
 
450
409
  if (!contractStartDate) {
451
410
  this.adapter.log.error(`❌ Kein Vertragsbeginn für ${basePath} konfiguriert.`);
@@ -497,42 +456,49 @@ class BillingManager {
497
456
  }
498
457
 
499
458
  /**
500
- * Updates billing countdown
459
+ * Updates billing countdown for all meters of a type
460
+ * NOTE: Since v1.4.6, this updates ALL meters (main + additional)
501
461
  *
502
462
  * @param {string} type - Utility type
503
463
  */
504
464
  async updateBillingCountdown(type) {
505
- const configType = this.adapter.consumptionManager.getConfigType(type);
506
- const contractStart = this.adapter.config[`${configType}ContractStart`];
465
+ // Get all meters for this type
466
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
507
467
 
508
- if (!contractStart) {
509
- return;
510
- }
468
+ // Update countdown for each meter based on its contract date
469
+ for (const meter of meters) {
470
+ const contractStart = meter.config?.contractStart;
511
471
 
512
- const startDate = calculator.parseGermanDate(contractStart);
513
- if (!startDate) {
514
- return;
515
- }
472
+ if (!contractStart) {
473
+ continue;
474
+ }
516
475
 
517
- const today = new Date();
518
- const nextAnniversary = new Date(startDate);
519
- nextAnniversary.setFullYear(today.getFullYear());
476
+ const startDate = calculator.parseGermanDate(contractStart);
477
+ if (!startDate) {
478
+ continue;
479
+ }
520
480
 
521
- if (nextAnniversary < today) {
522
- nextAnniversary.setFullYear(today.getFullYear() + 1);
523
- }
481
+ const today = new Date();
482
+ const nextAnniversary = new Date(startDate);
483
+ nextAnniversary.setFullYear(today.getFullYear());
524
484
 
525
- const msPerDay = 1000 * 60 * 60 * 24;
526
- const daysRemaining = Math.ceil((nextAnniversary.getTime() - today.getTime()) / msPerDay);
527
- const displayPeriodEnd = new Date(nextAnniversary);
528
- displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
485
+ if (nextAnniversary < today) {
486
+ nextAnniversary.setFullYear(today.getFullYear() + 1);
487
+ }
529
488
 
530
- await this.adapter.setStateAsync(`${type}.billing.daysRemaining`, daysRemaining, true);
531
- await this.adapter.setStateAsync(
532
- `${type}.billing.periodEnd`,
533
- displayPeriodEnd.toLocaleDateString('de-DE'),
534
- true,
535
- );
489
+ const msPerDay = 1000 * 60 * 60 * 24;
490
+ const daysRemaining = Math.ceil((nextAnniversary.getTime() - today.getTime()) / msPerDay);
491
+ const displayPeriodEnd = new Date(nextAnniversary);
492
+ displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
493
+
494
+ const basePath = `${type}.${meter.name}`;
495
+ await this.adapter.setStateAsync(`${basePath}.billing.daysRemaining`, daysRemaining, true);
496
+ await this.adapter.setStateAsync(
497
+ `${basePath}.billing.periodEnd`,
498
+ displayPeriodEnd.toLocaleDateString('de-DE'),
499
+ true,
500
+ );
501
+ }
536
502
  }
537
503
 
538
504
  /**
@@ -547,7 +513,7 @@ class BillingManager {
547
513
  const types = ['gas', 'water', 'electricity', 'pv'];
548
514
 
549
515
  for (const type of types) {
550
- const configType = this.adapter.consumptionManager.getConfigType(type);
516
+ const configType = getConfigType(type);
551
517
  if (!this.adapter.config[`${configType}Aktiv`]) {
552
518
  continue;
553
519
  }
@@ -559,72 +525,157 @@ class BillingManager {
559
525
 
560
526
  const nowDate = new Date(now);
561
527
 
562
- // DAILY RESET: All meters reset together at midnight
563
- const lastDayStart = await this.adapter.getStateAsync(`${type}.statistics.lastDayStart`);
528
+ // Get all meters for this type (needed for all reset checks)
529
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
530
+
531
+ if (meters.length === 0) {
532
+ continue;
533
+ }
534
+
535
+ const firstMeter = meters[0];
536
+ const basePath = `${type}.${firstMeter.name}`;
537
+
538
+ // Reset time window: 23:59 (last minute of the day)
539
+ // This ensures History adapter sees clean day boundaries
540
+ const isResetTime = nowDate.getHours() === 23 && nowDate.getMinutes() === 59;
541
+
542
+ // Helper: Check if timestamp is from today
543
+ const todayStart = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate()).getTime();
544
+ const isFromToday = timestamp => timestamp >= todayStart;
545
+
546
+ // DAILY RESET: Trigger at 23:59 if today's reset hasn't happened yet
547
+ const lastDayStart = await this.adapter.getStateAsync(`${basePath}.statistics.lastDayStart`);
564
548
  if (lastDayStart?.val) {
565
- const lastDay = new Date(lastDayStart.val);
566
- if (nowDate.getDate() !== lastDay.getDate()) {
549
+ const lastResetTime = lastDayStart.val;
550
+ const alreadyResetToday = isFromToday(lastResetTime);
551
+
552
+ // Reset at 23:59 if not yet reset today, OR catch up if we missed it (e.g. adapter was offline)
553
+ if (isResetTime && !alreadyResetToday) {
554
+ this.adapter.log.info(`Täglicher Reset für ${type} um 23:59`);
555
+ await this.resetDailyCounters(type);
556
+ } else if (!alreadyResetToday && nowDate.getTime() > lastResetTime + 24 * 60 * 60 * 1000) {
557
+ // Catch-up: More than 24h since last reset (adapter was offline)
558
+ this.adapter.log.info(`Täglicher Reset für ${type} (Nachholung - Adapter war offline)`);
567
559
  await this.resetDailyCounters(type);
568
560
  }
569
561
  }
570
562
 
571
- // MONTHLY RESET: All meters reset together on 1st of month
572
- const lastMonthStart = await this.adapter.getStateAsync(`${type}.statistics.lastMonthStart`);
573
- if (lastMonthStart?.val) {
574
- const lastMonth = new Date(lastMonthStart.val);
575
- if (nowDate.getMonth() !== lastMonth.getMonth()) {
576
- await this.resetMonthlyCounters(type);
563
+ // WEEKLY RESET: Trigger at 23:59 on Sunday if this week's reset hasn't happened yet
564
+ const lastWeekStart = await this.adapter.getStateAsync(`${basePath}.statistics.lastWeekStart`);
565
+ if (lastWeekStart?.val) {
566
+ const lastWeekTime = lastWeekStart.val;
567
+ const isSunday = nowDate.getDay() === 0; // 0 = Sunday
568
+
569
+ // Check if we're in a new week (more than 6 days since last reset)
570
+ const daysSinceLastReset = (nowDate.getTime() - lastWeekTime) / (24 * 60 * 60 * 1000);
571
+ const needsWeeklyReset = daysSinceLastReset >= 6;
572
+
573
+ if (isSunday && isResetTime && needsWeeklyReset) {
574
+ this.adapter.log.info(`Wöchentlicher Reset für ${type} um 23:59`);
575
+ await this.resetWeeklyCounters(type);
576
+ } else if (needsWeeklyReset && daysSinceLastReset > 7) {
577
+ // Catch-up: More than 7 days since last reset
578
+ this.adapter.log.info(`Wöchentlicher Reset für ${type} (Nachholung)`);
579
+ await this.resetWeeklyCounters(type);
580
+ }
581
+ }
582
+
583
+ // MONTHLY RESET: Trigger at 23:59 on last day of month
584
+ if (meters.length > 0) {
585
+ const lastMonthStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastMonthStart`);
586
+ if (lastMonthStartState?.val) {
587
+ const lastMonthTime = lastMonthStartState.val;
588
+ const lastMonthDate = new Date(lastMonthTime);
589
+ const isLastDayOfMonth =
590
+ new Date(nowDate.getFullYear(), nowDate.getMonth() + 1, 0).getDate() === nowDate.getDate();
591
+ const monthChanged = nowDate.getMonth() !== lastMonthDate.getMonth();
592
+
593
+ if (isLastDayOfMonth && isResetTime && monthChanged) {
594
+ this.adapter.log.info(`Monatlicher Reset für ${type} um 23:59`);
595
+ await this.resetMonthlyCounters(type);
596
+ } else if (monthChanged && nowDate.getDate() > 1) {
597
+ // Catch-up: We're past the 1st of a new month and haven't reset yet
598
+ this.adapter.log.info(`Monatlicher Reset für ${type} (Nachholung)`);
599
+ await this.resetMonthlyCounters(type);
600
+ }
577
601
  }
578
602
  }
579
603
 
580
604
  // YEARLY RESET: Each meter resets individually based on ITS contract date
581
- const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
605
+ // Trigger at 23:59 on the day BEFORE the anniversary (so the new year starts fresh)
582
606
  for (const meter of meters) {
583
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
584
- const lastYearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
607
+ const meterBasePath = `${type}.${meter.name}`;
608
+ const lastYearStartState = await this.adapter.getStateAsync(
609
+ `${meterBasePath}.statistics.lastYearStart`,
610
+ );
585
611
 
586
612
  if (lastYearStartState?.val) {
587
613
  const lastYearStartDate = new Date(lastYearStartState.val);
588
-
589
- // Get contract date for THIS specific meter
590
- let contractStartDate;
591
- if (meter.name === 'main') {
592
- // Main meter: use adapter config
593
- contractStartDate = this.adapter.config[`${configType}ContractStart`];
594
- } else {
595
- // Additional meter: use meter's individual config
596
- contractStartDate = meter.config?.contractStart;
597
- }
614
+ const contractStartDate = meter.config?.contractStart;
598
615
 
599
616
  if (contractStartDate) {
600
617
  const contractStart = calculator.parseGermanDate(contractStartDate);
601
618
  if (contractStart) {
602
619
  const annMonth = contractStart.getMonth();
603
620
  const annDay = contractStart.getDate();
604
- const isPast =
621
+
622
+ // Check if today is the day BEFORE the anniversary (for 23:59 reset)
623
+ // or if we're past it and haven't reset yet (catch-up)
624
+ const anniversaryThisYear = new Date(nowDate.getFullYear(), annMonth, annDay);
625
+ const dayBeforeAnniversary = new Date(anniversaryThisYear.getTime() - 24 * 60 * 60 * 1000);
626
+
627
+ const isTodayDayBefore =
628
+ nowDate.getMonth() === dayBeforeAnniversary.getMonth() &&
629
+ nowDate.getDate() === dayBeforeAnniversary.getDate();
630
+
631
+ const isPastAnniversary =
605
632
  nowDate.getMonth() > annMonth ||
606
633
  (nowDate.getMonth() === annMonth && nowDate.getDate() >= annDay);
607
634
 
608
- if (isPast && lastYearStartDate.getFullYear() !== nowDate.getFullYear()) {
635
+ const needsReset = lastYearStartDate.getFullYear() < nowDate.getFullYear();
636
+
637
+ if (isTodayDayBefore && isResetTime && needsReset) {
638
+ this.adapter.log.info(
639
+ `Yearly reset for ${meterBasePath} um 23:59 (Vertragsjubiläum morgen: ${contractStartDate})`,
640
+ );
641
+ await this.resetYearlyCountersForMeter(type, meter);
642
+
643
+ if (meters.length > 1) {
644
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
645
+ }
646
+ } else if (isPastAnniversary && needsReset) {
647
+ // Catch-up: Anniversary has passed but we haven't reset yet
609
648
  this.adapter.log.info(
610
- `Yearly reset for ${basePath} (contract anniversary: ${contractStartDate})`,
649
+ `Yearly reset for ${meterBasePath} (Nachholung - Jubiläum: ${contractStartDate})`,
611
650
  );
612
651
  await this.resetYearlyCountersForMeter(type, meter);
613
652
 
614
- // Update totals if multiple meters exist
615
653
  if (meters.length > 1) {
616
654
  await this.adapter.multiMeterManager.updateTotalCosts(type);
617
655
  }
618
656
  }
619
657
  }
620
- } else if (nowDate.getFullYear() !== lastYearStartDate.getFullYear()) {
621
- // No contract date: reset on January 1st
622
- this.adapter.log.info(`Yearly reset for ${basePath} (calendar year)`);
623
- await this.resetYearlyCountersForMeter(type, meter);
624
-
625
- // Update totals if multiple meters exist
626
- if (meters.length > 1) {
627
- await this.adapter.multiMeterManager.updateTotalCosts(type);
658
+ } else {
659
+ // No contract date: reset at 23:59 on December 31st (or catch up in January)
660
+ const isDecember31 = nowDate.getMonth() === 11 && nowDate.getDate() === 31;
661
+ const needsReset = nowDate.getFullYear() > lastYearStartDate.getFullYear();
662
+
663
+ if (isDecember31 && isResetTime && !needsReset) {
664
+ // Reset at 23:59 on Dec 31 for the upcoming year
665
+ this.adapter.log.info(`Yearly reset for ${meterBasePath} um 23:59 (Kalenderjahr)`);
666
+ await this.resetYearlyCountersForMeter(type, meter);
667
+
668
+ if (meters.length > 1) {
669
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
670
+ }
671
+ } else if (needsReset) {
672
+ // Catch-up: We're in a new year but haven't reset yet
673
+ this.adapter.log.info(`Yearly reset for ${meterBasePath} (Nachholung - Kalenderjahr)`);
674
+ await this.resetYearlyCountersForMeter(type, meter);
675
+
676
+ if (meters.length > 1) {
677
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
678
+ }
628
679
  }
629
680
  }
630
681
  }
@@ -650,7 +701,7 @@ class BillingManager {
650
701
 
651
702
  // Reset each meter
652
703
  for (const meter of meters) {
653
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
704
+ const basePath = `${type}.${meter.name}`;
654
705
  const label = meter.displayName || meter.name;
655
706
 
656
707
  this.adapter.log.debug(`Resetting daily counter for ${basePath} (${label})`);
@@ -672,7 +723,7 @@ class BillingManager {
672
723
  }
673
724
 
674
725
  // Reset HT/NT daily counters if enabled
675
- const configType = this.adapter.consumptionManager.getConfigType(type);
726
+ const configType = getConfigType(type);
676
727
  const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
677
728
  if (htNtEnabled) {
678
729
  const dailyHT = await this.adapter.getStateAsync(`${basePath}.consumption.dailyHT`);
@@ -719,22 +770,31 @@ class BillingManager {
719
770
 
720
771
  // Reset each meter
721
772
  for (const meter of meters) {
722
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
773
+ const basePath = `${type}.${meter.name}`;
723
774
  const label = meter.displayName || meter.name;
724
775
 
725
776
  this.adapter.log.debug(`Resetting monthly counter for ${basePath} (${label})`);
726
777
 
778
+ // Get current values before reset
727
779
  const monthlyState = await this.adapter.getStateAsync(`${basePath}.consumption.monthly`);
728
780
  const monthlyValue = monthlyState?.val || 0;
729
781
 
730
- await this.adapter.setStateAsync(`${basePath}.consumption.monthly`, 0, true);
782
+ // Save last month consumption
783
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastMonth`, monthlyValue, true);
731
784
 
785
+ // For gas: also save volume
732
786
  if (type === 'gas') {
787
+ const monthlyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.monthlyVolume`);
788
+ const monthlyVolumeValue = monthlyVolume?.val || 0;
789
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastMonthVolume`, monthlyVolumeValue, true);
733
790
  await this.adapter.setStateAsync(`${basePath}.consumption.monthlyVolume`, 0, true);
734
791
  }
735
792
 
793
+ // Reset monthly counters
794
+ await this.adapter.setStateAsync(`${basePath}.consumption.monthly`, 0, true);
795
+
736
796
  // Reset HT/NT monthly counters if enabled
737
- const configType = this.adapter.consumptionManager.getConfigType(type);
797
+ const configType = getConfigType(type);
738
798
  const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
739
799
  if (htNtEnabled) {
740
800
  await this.adapter.setStateAsync(`${basePath}.consumption.monthlyHT`, 0, true);
@@ -777,7 +837,7 @@ class BillingManager {
777
837
 
778
838
  // Reset each meter
779
839
  for (const meter of meters) {
780
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
840
+ const basePath = `${type}.${meter.name}`;
781
841
  const label = meter.displayName || meter.name;
782
842
 
783
843
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
@@ -809,7 +869,7 @@ class BillingManager {
809
869
  * @param {object} meter - Meter object from multiMeterManager
810
870
  */
811
871
  async resetYearlyCountersForMeter(type, meter) {
812
- const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
872
+ const basePath = `${type}.${meter.name}`;
813
873
  const label = meter.displayName || meter.name;
814
874
 
815
875
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
@@ -827,6 +887,52 @@ class BillingManager {
827
887
  // Update lastYearStart timestamp
828
888
  await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, Date.now(), true);
829
889
  }
890
+
891
+ /**
892
+ * Resets weekly counters
893
+ *
894
+ * @param {string} type - Utility type
895
+ */
896
+ async resetWeeklyCounters(type) {
897
+ this.adapter.log.info(`Resetting weekly counters for ${type}`);
898
+
899
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
900
+ for (const meter of meters) {
901
+ const basePath = `${type}.${meter.name}`;
902
+
903
+ // Save last week consumption before reset
904
+ const weeklyState = await this.adapter.getStateAsync(`${basePath}.consumption.weekly`);
905
+ const weeklyValue = weeklyState?.val || 0;
906
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastWeek`, weeklyValue, true);
907
+
908
+ // For gas: also save volume
909
+ if (type === 'gas') {
910
+ const weeklyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.weeklyVolume`);
911
+ const weeklyVolumeValue = weeklyVolume?.val || 0;
912
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastWeekVolume`, weeklyVolumeValue, true);
913
+ await this.adapter.setStateAsync(`${basePath}.consumption.weeklyVolume`, 0, true);
914
+ }
915
+
916
+ // Reset weekly counters
917
+ await this.adapter.setStateAsync(`${basePath}.consumption.weekly`, 0, true);
918
+ await this.adapter.setStateAsync(`${basePath}.costs.weekly`, 0, true);
919
+
920
+ const configType = getConfigType(type);
921
+ const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
922
+ if (htNtEnabled) {
923
+ await this.adapter.setStateAsync(`${basePath}.consumption.weeklyHT`, 0, true);
924
+ await this.adapter.setStateAsync(`${basePath}.consumption.weeklyNT`, 0, true);
925
+ await this.adapter.setStateAsync(`${basePath}.costs.weeklyHT`, 0, true);
926
+ await this.adapter.setStateAsync(`${basePath}.costs.weeklyNT`, 0, true);
927
+ }
928
+
929
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastWeekStart`, Date.now(), true);
930
+ }
931
+
932
+ if (meters.length > 1) {
933
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
934
+ }
935
+ }
830
936
  }
831
937
 
832
938
  module.exports = BillingManager;