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.
- package/README.md +159 -44
- package/admin/custom/.vite/manifest.json +90 -0
- package/admin/custom/@mf-types/Components.d.ts +2 -0
- package/admin/custom/@mf-types/compiled-types/Components/CSVImporter.d.ts +11 -0
- package/admin/custom/@mf-types/compiled-types/Components.d.ts +2 -0
- package/admin/custom/@mf-types.d.ts +3 -0
- package/admin/custom/@mf-types.zip +0 -0
- package/admin/custom/CSVImporter_v15_11.js +4415 -0
- package/admin/custom/assets/Components-i0AZ59nl.js +18887 -0
- package/admin/custom/assets/UtilityMonitor__loadShare__react__loadShare__-Da99Mak4.js +42 -0
- package/admin/custom/assets/UtilityMonitor__mf_v__runtimeInit__mf_v__-BmC4OGk6.js +16 -0
- package/admin/custom/assets/_commonjsHelpers-Dj2_voLF.js +30 -0
- package/admin/custom/assets/hostInit-DEXfeB0W.js +10 -0
- package/admin/custom/assets/index-B3WVNJTz.js +401 -0
- package/admin/custom/assets/index-VBwl8x_k.js +64 -0
- package/admin/custom/assets/preload-helper-BelkbqnE.js +61 -0
- package/admin/custom/assets/virtualExposes-CqCLUNLT.js +19 -0
- package/admin/custom/index.html +12 -0
- package/admin/custom/mf-manifest.json +1 -0
- package/admin/jsonConfig.json +219 -35
- package/io-package.json +51 -2
- package/lib/billingManager.js +276 -170
- package/lib/calculator.js +19 -138
- package/lib/consumptionManager.js +48 -331
- package/lib/importManager.js +300 -0
- package/lib/messagingHandler.js +112 -49
- package/lib/meter/MeterRegistry.js +110 -0
- package/lib/multiMeterManager.js +410 -181
- package/lib/stateManager.js +508 -36
- package/lib/utils/billingHelper.js +69 -0
- package/lib/utils/consumptionHelper.js +47 -0
- package/lib/utils/helpers.js +178 -0
- package/lib/utils/typeMapper.js +19 -0
- package/main.js +99 -36
- package/package.json +10 -4
package/lib/billingManager.js
CHANGED
|
@@ -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 =
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
102
|
-
await this.adapter.setStateAsync(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const totalYearlyCost = yearlyConsumptionCost +
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
const
|
|
465
|
+
// Get all meters for this type
|
|
466
|
+
const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
|
|
507
467
|
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
472
|
+
if (!contractStart) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
516
475
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
476
|
+
const startDate = calculator.parseGermanDate(contractStart);
|
|
477
|
+
if (!startDate) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
520
480
|
|
|
521
|
-
|
|
522
|
-
nextAnniversary
|
|
523
|
-
|
|
481
|
+
const today = new Date();
|
|
482
|
+
const nextAnniversary = new Date(startDate);
|
|
483
|
+
nextAnniversary.setFullYear(today.getFullYear());
|
|
524
484
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
|
|
485
|
+
if (nextAnniversary < today) {
|
|
486
|
+
nextAnniversary.setFullYear(today.getFullYear() + 1);
|
|
487
|
+
}
|
|
529
488
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
displayPeriodEnd.
|
|
534
|
-
|
|
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 =
|
|
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
|
-
//
|
|
563
|
-
const
|
|
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
|
|
566
|
-
|
|
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
|
-
//
|
|
572
|
-
const
|
|
573
|
-
if (
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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
|
|
584
|
-
const lastYearStartState = await this.adapter.getStateAsync(
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
621
|
-
// No contract date: reset on January
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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;
|