iobroker.utility-monitor 1.4.6 → 1.5.1
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 +110 -62
- 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 +90 -31
- package/io-package.json +15 -31
- package/lib/billingManager.js +382 -137
- package/lib/calculator.js +41 -146
- package/lib/consumptionManager.js +9 -252
- package/lib/importManager.js +300 -0
- package/lib/messagingHandler.js +4 -2
- package/lib/meter/MeterRegistry.js +110 -0
- package/lib/multiMeterManager.js +580 -173
- package/lib/stateManager.js +502 -31
- package/lib/utils/billingHelper.js +69 -0
- package/lib/utils/consumptionHelper.js +47 -0
- package/lib/utils/helpers.js +234 -0
- package/lib/utils/stateCache.js +147 -0
- package/lib/utils/typeMapper.js +19 -0
- package/main.js +67 -8
- 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
|
-
}
|
|
157
|
-
|
|
158
|
-
// Calculate accumulated basic charge (monthly fee × months)
|
|
159
|
-
const basicChargeAccumulated = basicChargeMonthly * monthsSinceContract;
|
|
114
|
+
const monthsSinceContract = await this._calculateMonthsSinceStart(type, configType);
|
|
160
115
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
|
@@ -454,11 +419,96 @@ class BillingManager {
|
|
|
454
419
|
return;
|
|
455
420
|
}
|
|
456
421
|
|
|
422
|
+
// Archive data for this meter
|
|
457
423
|
const year = startDate.getFullYear();
|
|
458
424
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
//
|
|
425
|
+
this.adapter.log.info(`Archiving data for ${basePath} year ${year}...`);
|
|
426
|
+
|
|
427
|
+
// Create history structure for this meter
|
|
428
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.history`, {
|
|
429
|
+
type: 'channel',
|
|
430
|
+
common: { name: 'Historie' },
|
|
431
|
+
native: {},
|
|
432
|
+
});
|
|
433
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}`, {
|
|
434
|
+
type: 'channel',
|
|
435
|
+
common: { name: `Jahr ${year}` },
|
|
436
|
+
native: {},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Get current values for archiving
|
|
440
|
+
const yearlyState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
|
|
441
|
+
const totalYearlyState = await this.adapter.getStateAsync(`${basePath}.costs.totalYearly`);
|
|
442
|
+
const balanceState = await this.adapter.getStateAsync(`${basePath}.costs.balance`);
|
|
443
|
+
|
|
444
|
+
const yearly = yearlyState?.val || 0;
|
|
445
|
+
const totalYearly = totalYearlyState?.val || 0;
|
|
446
|
+
const balance = balanceState?.val || 0;
|
|
447
|
+
|
|
448
|
+
const consumptionUnit = type === 'gas' ? 'kWh' : type === 'water' ? 'm³' : 'kWh';
|
|
449
|
+
|
|
450
|
+
// Archive yearly consumption
|
|
451
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.yearly`, {
|
|
452
|
+
type: 'state',
|
|
453
|
+
common: {
|
|
454
|
+
name: `Jahresverbrauch ${year}`,
|
|
455
|
+
type: 'number',
|
|
456
|
+
role: 'value',
|
|
457
|
+
read: true,
|
|
458
|
+
write: false,
|
|
459
|
+
unit: consumptionUnit,
|
|
460
|
+
},
|
|
461
|
+
native: {},
|
|
462
|
+
});
|
|
463
|
+
await this.adapter.setStateAsync(`${basePath}.history.${year}.yearly`, yearly, true);
|
|
464
|
+
|
|
465
|
+
// Archive gas volume if applicable
|
|
466
|
+
if (type === 'gas') {
|
|
467
|
+
const yearlyVolume = (await this.adapter.getStateAsync(`${basePath}.consumption.yearlyVolume`))?.val || 0;
|
|
468
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.yearlyVolume`, {
|
|
469
|
+
type: 'state',
|
|
470
|
+
common: {
|
|
471
|
+
name: `Jahresverbrauch ${year} (m³)`,
|
|
472
|
+
type: 'number',
|
|
473
|
+
role: 'value',
|
|
474
|
+
read: true,
|
|
475
|
+
write: false,
|
|
476
|
+
unit: 'm³',
|
|
477
|
+
},
|
|
478
|
+
native: {},
|
|
479
|
+
});
|
|
480
|
+
await this.adapter.setStateAsync(`${basePath}.history.${year}.yearlyVolume`, yearlyVolume, true);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Archive total yearly costs
|
|
484
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.totalYearly`, {
|
|
485
|
+
type: 'state',
|
|
486
|
+
common: {
|
|
487
|
+
name: `Gesamtkosten ${year}`,
|
|
488
|
+
type: 'number',
|
|
489
|
+
role: 'value.money',
|
|
490
|
+
read: true,
|
|
491
|
+
write: false,
|
|
492
|
+
unit: '€',
|
|
493
|
+
},
|
|
494
|
+
native: {},
|
|
495
|
+
});
|
|
496
|
+
await this.adapter.setStateAsync(`${basePath}.history.${year}.totalYearly`, totalYearly, true);
|
|
497
|
+
|
|
498
|
+
// Archive balance
|
|
499
|
+
await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.balance`, {
|
|
500
|
+
type: 'state',
|
|
501
|
+
common: {
|
|
502
|
+
name: `Bilanz ${year}`,
|
|
503
|
+
type: 'number',
|
|
504
|
+
role: 'value.money',
|
|
505
|
+
read: true,
|
|
506
|
+
write: false,
|
|
507
|
+
unit: '€',
|
|
508
|
+
},
|
|
509
|
+
native: {},
|
|
510
|
+
});
|
|
511
|
+
await this.adapter.setStateAsync(`${basePath}.history.${year}.balance`, balance, true);
|
|
462
512
|
|
|
463
513
|
// Reset consumption and costs for this meter
|
|
464
514
|
await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, 0, true);
|
|
@@ -548,7 +598,7 @@ class BillingManager {
|
|
|
548
598
|
const types = ['gas', 'water', 'electricity', 'pv'];
|
|
549
599
|
|
|
550
600
|
for (const type of types) {
|
|
551
|
-
const configType =
|
|
601
|
+
const configType = getConfigType(type);
|
|
552
602
|
if (!this.adapter.config[`${configType}Aktiv`]) {
|
|
553
603
|
continue;
|
|
554
604
|
}
|
|
@@ -560,34 +610,126 @@ class BillingManager {
|
|
|
560
610
|
|
|
561
611
|
const nowDate = new Date(now);
|
|
562
612
|
|
|
563
|
-
//
|
|
564
|
-
const
|
|
613
|
+
// Get all meters for this type (needed for all reset checks)
|
|
614
|
+
const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
|
|
615
|
+
|
|
616
|
+
if (meters.length === 0) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const firstMeter = meters[0];
|
|
621
|
+
const basePath = `${type}.${firstMeter.name}`;
|
|
622
|
+
|
|
623
|
+
// Reset time window: 23:59 (last minute of the day)
|
|
624
|
+
// This ensures History adapter sees clean day boundaries
|
|
625
|
+
const isResetTime = nowDate.getHours() === 23 && nowDate.getMinutes() === 59;
|
|
626
|
+
|
|
627
|
+
// Helper: Create normalized timestamp for 23:59:00 of a given date
|
|
628
|
+
// This ensures consistent timestamps regardless of actual execution time
|
|
629
|
+
const getNormalizedResetTime = date => {
|
|
630
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 0, 0).getTime();
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// Helper: Check if timestamp is from a valid 23:59 reset today
|
|
634
|
+
// A valid reset must be from today AND from the 23:xx hour
|
|
635
|
+
// This prevents adapter restarts (e.g. at 00:01:03) from blocking the real 23:59 reset
|
|
636
|
+
const todayStart = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate()).getTime();
|
|
637
|
+
const isValidResetToday = timestamp => {
|
|
638
|
+
if (timestamp < todayStart) {
|
|
639
|
+
return false; // Not from today
|
|
640
|
+
}
|
|
641
|
+
// Check if the timestamp was from the 23:xx hour (valid reset time)
|
|
642
|
+
const resetDate = new Date(timestamp);
|
|
643
|
+
return resetDate.getHours() === 23;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
// DAILY RESET: Trigger at 23:59 if today's reset hasn't happened yet
|
|
647
|
+
const lastDayStart = await this.adapter.getStateAsync(`${basePath}.statistics.lastDayStart`);
|
|
565
648
|
if (lastDayStart?.val) {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
649
|
+
const lastResetTime = lastDayStart.val;
|
|
650
|
+
const alreadyResetToday = isValidResetToday(lastResetTime);
|
|
651
|
+
|
|
652
|
+
// Reset at 23:59 if not yet reset today, OR catch up if we missed it (e.g. adapter was offline)
|
|
653
|
+
if (isResetTime && !alreadyResetToday) {
|
|
654
|
+
this.adapter.log.info(`Täglicher Reset für ${type} um 23:59`);
|
|
655
|
+
await this.resetDailyCounters(type, getNormalizedResetTime(nowDate));
|
|
656
|
+
} else if (!alreadyResetToday && nowDate.getTime() > lastResetTime + 24 * 60 * 60 * 1000) {
|
|
657
|
+
// Catch-up: More than 24h since last reset (adapter was offline)
|
|
658
|
+
// Calculate when the reset SHOULD have happened (yesterday 23:59)
|
|
659
|
+
const yesterday = new Date(nowDate);
|
|
660
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
661
|
+
const catchUpTime = getNormalizedResetTime(yesterday);
|
|
662
|
+
this.adapter.log.info(`Täglicher Reset für ${type} (Nachholung - Adapter war offline)`);
|
|
663
|
+
await this.resetDailyCounters(type, catchUpTime);
|
|
569
664
|
}
|
|
570
665
|
}
|
|
571
666
|
|
|
572
|
-
//
|
|
573
|
-
const
|
|
574
|
-
if (
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
667
|
+
// WEEKLY RESET: Trigger at 23:59 on Sunday if this week's reset hasn't happened yet
|
|
668
|
+
const lastWeekStart = await this.adapter.getStateAsync(`${basePath}.statistics.lastWeekStart`);
|
|
669
|
+
if (lastWeekStart?.val) {
|
|
670
|
+
const lastWeekTime = lastWeekStart.val;
|
|
671
|
+
const isSunday = nowDate.getDay() === 0; // 0 = Sunday
|
|
672
|
+
|
|
673
|
+
// Check if we're in a new week (more than 6 days since last reset)
|
|
674
|
+
// BUT also check if the last reset was actually a valid 23:59 reset
|
|
675
|
+
// to prevent adapter restarts from blocking the real weekly reset
|
|
676
|
+
const daysSinceLastReset = (nowDate.getTime() - lastWeekTime) / (24 * 60 * 60 * 1000);
|
|
677
|
+
const lastWeekResetDate = new Date(lastWeekTime);
|
|
678
|
+
const wasValidWeeklyReset = lastWeekResetDate.getHours() === 23;
|
|
679
|
+
const needsWeeklyReset = daysSinceLastReset >= 6 || (isSunday && !wasValidWeeklyReset);
|
|
680
|
+
|
|
681
|
+
if (isSunday && isResetTime && needsWeeklyReset) {
|
|
682
|
+
this.adapter.log.info(`Wöchentlicher Reset für ${type} um 23:59`);
|
|
683
|
+
await this.resetWeeklyCounters(type, getNormalizedResetTime(nowDate));
|
|
684
|
+
} else if (needsWeeklyReset && daysSinceLastReset >= 7) {
|
|
685
|
+
// Catch-up: 7 or more days since last reset (was offline or missed window)
|
|
686
|
+
// Calculate when the reset SHOULD have happened (last Sunday 23:59)
|
|
687
|
+
const lastSunday = new Date(nowDate);
|
|
688
|
+
lastSunday.setDate(lastSunday.getDate() - lastSunday.getDay()); // Go back to Sunday
|
|
689
|
+
const catchUpTime = getNormalizedResetTime(lastSunday);
|
|
690
|
+
this.adapter.log.info(`Wöchentlicher Reset für ${type} (Nachholung)`);
|
|
691
|
+
await this.resetWeeklyCounters(type, catchUpTime);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// MONTHLY RESET: Trigger at 23:59 on last day of month
|
|
696
|
+
if (meters.length > 0) {
|
|
697
|
+
const lastMonthStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastMonthStart`);
|
|
698
|
+
if (lastMonthStartState?.val) {
|
|
699
|
+
const lastMonthTime = lastMonthStartState.val;
|
|
700
|
+
const lastMonthDate = new Date(lastMonthTime);
|
|
701
|
+
const isLastDayOfMonth =
|
|
702
|
+
new Date(nowDate.getFullYear(), nowDate.getMonth() + 1, 0).getDate() === nowDate.getDate();
|
|
703
|
+
const monthChanged = nowDate.getMonth() !== lastMonthDate.getMonth();
|
|
704
|
+
// Also check if the last reset was a valid 23:59 reset
|
|
705
|
+
// to prevent adapter restarts from blocking the real monthly reset
|
|
706
|
+
const wasValidMonthlyReset = lastMonthDate.getHours() === 23;
|
|
707
|
+
const needsMonthlyReset = monthChanged || (isLastDayOfMonth && !wasValidMonthlyReset);
|
|
708
|
+
|
|
709
|
+
if (isLastDayOfMonth && isResetTime && needsMonthlyReset) {
|
|
710
|
+
this.adapter.log.info(`Monatlicher Reset für ${type} um 23:59`);
|
|
711
|
+
await this.resetMonthlyCounters(type, getNormalizedResetTime(nowDate));
|
|
712
|
+
} else if (monthChanged && nowDate.getDate() > 1) {
|
|
713
|
+
// Catch-up: We're past the 1st of a new month and haven't reset yet
|
|
714
|
+
// Calculate when the reset SHOULD have happened (last day of previous month 23:59)
|
|
715
|
+
const lastDayPrevMonth = new Date(nowDate.getFullYear(), nowDate.getMonth(), 0);
|
|
716
|
+
const catchUpTime = getNormalizedResetTime(lastDayPrevMonth);
|
|
717
|
+
this.adapter.log.info(`Monatlicher Reset für ${type} (Nachholung)`);
|
|
718
|
+
await this.resetMonthlyCounters(type, catchUpTime);
|
|
719
|
+
}
|
|
578
720
|
}
|
|
579
721
|
}
|
|
580
722
|
|
|
581
723
|
// YEARLY RESET: Each meter resets individually based on ITS contract date
|
|
582
|
-
|
|
724
|
+
// Trigger at 23:59 on the day BEFORE the anniversary (so the new year starts fresh)
|
|
583
725
|
for (const meter of meters) {
|
|
584
|
-
const
|
|
585
|
-
const lastYearStartState = await this.adapter.getStateAsync(
|
|
726
|
+
const meterBasePath = `${type}.${meter.name}`;
|
|
727
|
+
const lastYearStartState = await this.adapter.getStateAsync(
|
|
728
|
+
`${meterBasePath}.statistics.lastYearStart`,
|
|
729
|
+
);
|
|
586
730
|
|
|
587
731
|
if (lastYearStartState?.val) {
|
|
588
732
|
const lastYearStartDate = new Date(lastYearStartState.val);
|
|
589
|
-
|
|
590
|
-
// Get contract date for THIS specific meter (all meters have config.contractStart)
|
|
591
733
|
const contractStartDate = meter.config?.contractStart;
|
|
592
734
|
|
|
593
735
|
if (contractStartDate) {
|
|
@@ -595,30 +737,69 @@ class BillingManager {
|
|
|
595
737
|
if (contractStart) {
|
|
596
738
|
const annMonth = contractStart.getMonth();
|
|
597
739
|
const annDay = contractStart.getDate();
|
|
598
|
-
|
|
740
|
+
|
|
741
|
+
// Check if today is the day BEFORE the anniversary (for 23:59 reset)
|
|
742
|
+
// or if we're past it and haven't reset yet (catch-up)
|
|
743
|
+
const anniversaryThisYear = new Date(nowDate.getFullYear(), annMonth, annDay);
|
|
744
|
+
const dayBeforeAnniversary = new Date(anniversaryThisYear.getTime() - 24 * 60 * 60 * 1000);
|
|
745
|
+
|
|
746
|
+
const isTodayDayBefore =
|
|
747
|
+
nowDate.getMonth() === dayBeforeAnniversary.getMonth() &&
|
|
748
|
+
nowDate.getDate() === dayBeforeAnniversary.getDate();
|
|
749
|
+
|
|
750
|
+
const isPastAnniversary =
|
|
599
751
|
nowDate.getMonth() > annMonth ||
|
|
600
752
|
(nowDate.getMonth() === annMonth && nowDate.getDate() >= annDay);
|
|
601
753
|
|
|
602
|
-
|
|
754
|
+
const needsReset = lastYearStartDate.getFullYear() < nowDate.getFullYear();
|
|
755
|
+
|
|
756
|
+
if (isTodayDayBefore && isResetTime && needsReset) {
|
|
603
757
|
this.adapter.log.info(
|
|
604
|
-
`Yearly reset for ${
|
|
758
|
+
`Yearly reset for ${meterBasePath} um 23:59 (Vertragsjubiläum morgen: ${contractStartDate})`,
|
|
605
759
|
);
|
|
606
|
-
await this.resetYearlyCountersForMeter(type, meter);
|
|
760
|
+
await this.resetYearlyCountersForMeter(type, meter, getNormalizedResetTime(nowDate));
|
|
761
|
+
|
|
762
|
+
if (meters.length > 1) {
|
|
763
|
+
await this.adapter.multiMeterManager.updateTotalCosts(type);
|
|
764
|
+
}
|
|
765
|
+
} else if (isPastAnniversary && needsReset) {
|
|
766
|
+
// Catch-up: Anniversary has passed but we haven't reset yet
|
|
767
|
+
// Use the day before anniversary 23:59 as the reset time
|
|
768
|
+
const catchUpTime = getNormalizedResetTime(dayBeforeAnniversary);
|
|
769
|
+
this.adapter.log.info(
|
|
770
|
+
`Yearly reset for ${meterBasePath} (Nachholung - Jubiläum: ${contractStartDate})`,
|
|
771
|
+
);
|
|
772
|
+
await this.resetYearlyCountersForMeter(type, meter, catchUpTime);
|
|
607
773
|
|
|
608
|
-
// Update totals if multiple meters exist
|
|
609
774
|
if (meters.length > 1) {
|
|
610
775
|
await this.adapter.multiMeterManager.updateTotalCosts(type);
|
|
611
776
|
}
|
|
612
777
|
}
|
|
613
778
|
}
|
|
614
|
-
} else
|
|
615
|
-
// No contract date: reset on January
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
779
|
+
} else {
|
|
780
|
+
// No contract date: reset at 23:59 on December 31st (or catch up in January)
|
|
781
|
+
const isDecember31 = nowDate.getMonth() === 11 && nowDate.getDate() === 31;
|
|
782
|
+
const needsReset = nowDate.getFullYear() > lastYearStartDate.getFullYear();
|
|
783
|
+
|
|
784
|
+
if (isDecember31 && isResetTime && !needsReset) {
|
|
785
|
+
// Reset at 23:59 on Dec 31 for the upcoming year
|
|
786
|
+
this.adapter.log.info(`Yearly reset for ${meterBasePath} um 23:59 (Kalenderjahr)`);
|
|
787
|
+
await this.resetYearlyCountersForMeter(type, meter, getNormalizedResetTime(nowDate));
|
|
788
|
+
|
|
789
|
+
if (meters.length > 1) {
|
|
790
|
+
await this.adapter.multiMeterManager.updateTotalCosts(type);
|
|
791
|
+
}
|
|
792
|
+
} else if (needsReset) {
|
|
793
|
+
// Catch-up: We're in a new year but haven't reset yet
|
|
794
|
+
// Use Dec 31 of last year 23:59 as reset time
|
|
795
|
+
const dec31LastYear = new Date(nowDate.getFullYear() - 1, 11, 31);
|
|
796
|
+
const catchUpTime = getNormalizedResetTime(dec31LastYear);
|
|
797
|
+
this.adapter.log.info(`Yearly reset for ${meterBasePath} (Nachholung - Kalenderjahr)`);
|
|
798
|
+
await this.resetYearlyCountersForMeter(type, meter, catchUpTime);
|
|
799
|
+
|
|
800
|
+
if (meters.length > 1) {
|
|
801
|
+
await this.adapter.multiMeterManager.updateTotalCosts(type);
|
|
802
|
+
}
|
|
622
803
|
}
|
|
623
804
|
}
|
|
624
805
|
}
|
|
@@ -630,8 +811,9 @@ class BillingManager {
|
|
|
630
811
|
* Resets daily counters
|
|
631
812
|
*
|
|
632
813
|
* @param {string} type - Utility type
|
|
814
|
+
* @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
|
|
633
815
|
*/
|
|
634
|
-
async resetDailyCounters(type) {
|
|
816
|
+
async resetDailyCounters(type, resetTimestamp) {
|
|
635
817
|
this.adapter.log.info(`Resetting daily counters for ${type}`);
|
|
636
818
|
|
|
637
819
|
// Get all meters for this type (main + additional meters)
|
|
@@ -666,7 +848,7 @@ class BillingManager {
|
|
|
666
848
|
}
|
|
667
849
|
|
|
668
850
|
// Reset HT/NT daily counters if enabled
|
|
669
|
-
const configType =
|
|
851
|
+
const configType = getConfigType(type);
|
|
670
852
|
const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
|
|
671
853
|
if (htNtEnabled) {
|
|
672
854
|
const dailyHT = await this.adapter.getStateAsync(`${basePath}.consumption.dailyHT`);
|
|
@@ -679,8 +861,9 @@ class BillingManager {
|
|
|
679
861
|
|
|
680
862
|
await this.adapter.setStateAsync(`${basePath}.costs.daily`, 0, true);
|
|
681
863
|
|
|
682
|
-
// Update lastDayStart timestamp
|
|
683
|
-
|
|
864
|
+
// Update lastDayStart timestamp (use normalized timestamp if provided)
|
|
865
|
+
const timestamp = resetTimestamp || Date.now();
|
|
866
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastDayStart`, timestamp, true);
|
|
684
867
|
|
|
685
868
|
await this.adapter.setStateAsync(
|
|
686
869
|
`${basePath}.statistics.averageDaily`,
|
|
@@ -699,8 +882,9 @@ class BillingManager {
|
|
|
699
882
|
* Resets monthly counters
|
|
700
883
|
*
|
|
701
884
|
* @param {string} type - Utility type
|
|
885
|
+
* @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
|
|
702
886
|
*/
|
|
703
|
-
async resetMonthlyCounters(type) {
|
|
887
|
+
async resetMonthlyCounters(type, resetTimestamp) {
|
|
704
888
|
this.adapter.log.info(`Resetting monthly counters for ${type}`);
|
|
705
889
|
|
|
706
890
|
// Get all meters for this type (main + additional meters)
|
|
@@ -718,17 +902,26 @@ class BillingManager {
|
|
|
718
902
|
|
|
719
903
|
this.adapter.log.debug(`Resetting monthly counter for ${basePath} (${label})`);
|
|
720
904
|
|
|
905
|
+
// Get current values before reset
|
|
721
906
|
const monthlyState = await this.adapter.getStateAsync(`${basePath}.consumption.monthly`);
|
|
722
907
|
const monthlyValue = monthlyState?.val || 0;
|
|
723
908
|
|
|
724
|
-
|
|
909
|
+
// Save last month consumption
|
|
910
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastMonth`, monthlyValue, true);
|
|
725
911
|
|
|
912
|
+
// For gas: also save volume
|
|
726
913
|
if (type === 'gas') {
|
|
914
|
+
const monthlyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.monthlyVolume`);
|
|
915
|
+
const monthlyVolumeValue = monthlyVolume?.val || 0;
|
|
916
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastMonthVolume`, monthlyVolumeValue, true);
|
|
727
917
|
await this.adapter.setStateAsync(`${basePath}.consumption.monthlyVolume`, 0, true);
|
|
728
918
|
}
|
|
729
919
|
|
|
920
|
+
// Reset monthly counters
|
|
921
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.monthly`, 0, true);
|
|
922
|
+
|
|
730
923
|
// Reset HT/NT monthly counters if enabled
|
|
731
|
-
const configType =
|
|
924
|
+
const configType = getConfigType(type);
|
|
732
925
|
const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
|
|
733
926
|
if (htNtEnabled) {
|
|
734
927
|
await this.adapter.setStateAsync(`${basePath}.consumption.monthlyHT`, 0, true);
|
|
@@ -737,8 +930,9 @@ class BillingManager {
|
|
|
737
930
|
|
|
738
931
|
await this.adapter.setStateAsync(`${basePath}.costs.monthly`, 0, true);
|
|
739
932
|
|
|
740
|
-
// Update lastMonthStart timestamp
|
|
741
|
-
|
|
933
|
+
// Update lastMonthStart timestamp (use normalized timestamp if provided)
|
|
934
|
+
const timestamp = resetTimestamp || Date.now();
|
|
935
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastMonthStart`, timestamp, true);
|
|
742
936
|
|
|
743
937
|
await this.adapter.setStateAsync(
|
|
744
938
|
`${basePath}.statistics.averageMonthly`,
|
|
@@ -801,8 +995,9 @@ class BillingManager {
|
|
|
801
995
|
*
|
|
802
996
|
* @param {string} type - Utility type
|
|
803
997
|
* @param {object} meter - Meter object from multiMeterManager
|
|
998
|
+
* @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
|
|
804
999
|
*/
|
|
805
|
-
async resetYearlyCountersForMeter(type, meter) {
|
|
1000
|
+
async resetYearlyCountersForMeter(type, meter, resetTimestamp) {
|
|
806
1001
|
const basePath = `${type}.${meter.name}`;
|
|
807
1002
|
const label = meter.displayName || meter.name;
|
|
808
1003
|
|
|
@@ -818,8 +1013,58 @@ class BillingManager {
|
|
|
818
1013
|
await this.adapter.setStateAsync(`${basePath}.billing.notificationSent`, false, true);
|
|
819
1014
|
await this.adapter.setStateAsync(`${basePath}.billing.notificationChangeSent`, false, true);
|
|
820
1015
|
|
|
821
|
-
// Update lastYearStart timestamp
|
|
822
|
-
|
|
1016
|
+
// Update lastYearStart timestamp (use normalized timestamp if provided)
|
|
1017
|
+
const timestamp = resetTimestamp || Date.now();
|
|
1018
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, timestamp, true);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Resets weekly counters
|
|
1023
|
+
*
|
|
1024
|
+
* @param {string} type - Utility type
|
|
1025
|
+
* @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
|
|
1026
|
+
*/
|
|
1027
|
+
async resetWeeklyCounters(type, resetTimestamp) {
|
|
1028
|
+
this.adapter.log.info(`Resetting weekly counters for ${type}`);
|
|
1029
|
+
|
|
1030
|
+
const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
|
|
1031
|
+
for (const meter of meters) {
|
|
1032
|
+
const basePath = `${type}.${meter.name}`;
|
|
1033
|
+
|
|
1034
|
+
// Save last week consumption before reset
|
|
1035
|
+
const weeklyState = await this.adapter.getStateAsync(`${basePath}.consumption.weekly`);
|
|
1036
|
+
const weeklyValue = weeklyState?.val || 0;
|
|
1037
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastWeek`, weeklyValue, true);
|
|
1038
|
+
|
|
1039
|
+
// For gas: also save volume
|
|
1040
|
+
if (type === 'gas') {
|
|
1041
|
+
const weeklyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.weeklyVolume`);
|
|
1042
|
+
const weeklyVolumeValue = weeklyVolume?.val || 0;
|
|
1043
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastWeekVolume`, weeklyVolumeValue, true);
|
|
1044
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.weeklyVolume`, 0, true);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Reset weekly counters
|
|
1048
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.weekly`, 0, true);
|
|
1049
|
+
await this.adapter.setStateAsync(`${basePath}.costs.weekly`, 0, true);
|
|
1050
|
+
|
|
1051
|
+
const configType = getConfigType(type);
|
|
1052
|
+
const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
|
|
1053
|
+
if (htNtEnabled) {
|
|
1054
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.weeklyHT`, 0, true);
|
|
1055
|
+
await this.adapter.setStateAsync(`${basePath}.consumption.weeklyNT`, 0, true);
|
|
1056
|
+
await this.adapter.setStateAsync(`${basePath}.costs.weeklyHT`, 0, true);
|
|
1057
|
+
await this.adapter.setStateAsync(`${basePath}.costs.weeklyNT`, 0, true);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Update lastWeekStart timestamp (use normalized timestamp if provided)
|
|
1061
|
+
const timestamp = resetTimestamp || Date.now();
|
|
1062
|
+
await this.adapter.setStateAsync(`${basePath}.statistics.lastWeekStart`, timestamp, true);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (meters.length > 1) {
|
|
1066
|
+
await this.adapter.multiMeterManager.updateTotalCosts(type);
|
|
1067
|
+
}
|
|
823
1068
|
}
|
|
824
1069
|
}
|
|
825
1070
|
|