iobroker.utility-monitor 1.4.2

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.
@@ -0,0 +1,806 @@
1
+ 'use strict';
2
+
3
+ const calculator = require('./calculator');
4
+
5
+ /**
6
+ * BillingManager handles all cost calculations,
7
+ * billing period management, and automatic resets.
8
+ */
9
+ class BillingManager {
10
+ /**
11
+ * @param {object} adapter - ioBroker adapter instance
12
+ */
13
+ constructor(adapter) {
14
+ this.adapter = adapter;
15
+ }
16
+
17
+ /**
18
+ * Updates cost calculations for a utility type
19
+ *
20
+ * @param {string} type - Utility type
21
+ */
22
+ async updateCosts(type) {
23
+ const configType = this.adapter.consumptionManager.getConfigType(type);
24
+
25
+ // Get price and basic charge from config
26
+ const priceKey = `${configType}Preis`;
27
+ const grundgebuehrKey = `${configType}Grundgebuehr`;
28
+ const jahresgebuehrKey = `${configType}Jahresgebuehr`;
29
+ const price = this.adapter.config[priceKey] || 0;
30
+ const basicChargeMonthly = this.adapter.config[grundgebuehrKey] || 0;
31
+ const annualFeePerYear = this.adapter.config[jahresgebuehrKey] || 0;
32
+
33
+ const htNtEnabledKey = `${configType}HtNtEnabled`;
34
+ const htNtEnabled = this.adapter.config[htNtEnabledKey] || false;
35
+
36
+ if (price === 0 && !htNtEnabled) {
37
+ this.adapter.log.debug(`No price configured for ${type} (${configType}) and HT/NT is disabled`);
38
+ return;
39
+ }
40
+
41
+ // Get current consumptions
42
+ const dailyState = await this.adapter.getStateAsync(`${type}.consumption.daily`);
43
+ const monthlyState = await this.adapter.getStateAsync(`${type}.consumption.monthly`);
44
+ const yearlyState = await this.adapter.getStateAsync(`${type}.consumption.yearly`);
45
+
46
+ const daily = typeof dailyState?.val === 'number' ? dailyState.val : 0;
47
+ const monthly = typeof monthlyState?.val === 'number' ? monthlyState.val : 0;
48
+ let yearly = typeof yearlyState?.val === 'number' ? yearlyState.val : 0;
49
+
50
+ // Apply manual adjustment
51
+ const adjustmentState = await this.adapter.getStateAsync(`${type}.adjustment.value`);
52
+ const adjustment = typeof adjustmentState?.val === 'number' ? adjustmentState.val : 0;
53
+ if (adjustment !== 0) {
54
+ if (type === 'gas') {
55
+ const yearlyVolumeState = await this.adapter.getStateAsync(`${type}.consumption.yearlyVolume`);
56
+ const yearlyVolume = typeof yearlyVolumeState?.val === 'number' ? yearlyVolumeState.val : 0;
57
+ const totalM3 = yearlyVolume + adjustment;
58
+ const brennwert = this.adapter.config.gasBrennwert || 11.5;
59
+ const zZahl = this.adapter.config.gasZahl || 0.95;
60
+ yearly = calculator.convertGasM3ToKWh(totalM3, brennwert, zZahl);
61
+ } else {
62
+ yearly += adjustment;
63
+ }
64
+ }
65
+
66
+ // Consumption cost calculation
67
+ let dailyConsumptionCost, monthlyConsumptionCost, yearlyConsumptionCost;
68
+
69
+ if (htNtEnabled) {
70
+ // HT/NT Calculation
71
+ const htPrice = this.adapter.config[`${configType}HtPrice`] || 0;
72
+ const ntPrice = this.adapter.config[`${configType}NtPrice`] || 0;
73
+
74
+ const dailyHT = (await this.adapter.getStateAsync(`${type}.consumption.dailyHT`))?.val || 0;
75
+ const dailyNT = (await this.adapter.getStateAsync(`${type}.consumption.dailyNT`))?.val || 0;
76
+ const monthlyHT = (await this.adapter.getStateAsync(`${type}.consumption.monthlyHT`))?.val || 0;
77
+ const monthlyNT = (await this.adapter.getStateAsync(`${type}.consumption.monthlyNT`))?.val || 0;
78
+
79
+ let yearlyHT = (await this.adapter.getStateAsync(`${type}.consumption.yearlyHT`))?.val || 0;
80
+ const yearlyNT = (await this.adapter.getStateAsync(`${type}.consumption.yearlyNT`))?.val || 0;
81
+
82
+ if (adjustment !== 0) {
83
+ if (type === 'gas') {
84
+ const brennwert = this.adapter.config.gasBrennwert || 11.5;
85
+ const zZahl = this.adapter.config.gasZahl || 0.95;
86
+ yearlyHT = Number(yearlyHT) + calculator.convertGasM3ToKWh(adjustment, brennwert, zZahl);
87
+ } else {
88
+ yearlyHT = Number(yearlyHT) + Number(adjustment);
89
+ }
90
+ }
91
+
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);
95
+
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
+ );
127
+ } else {
128
+ dailyConsumptionCost = calculator.calculateCost(daily, price);
129
+ monthlyConsumptionCost = calculator.calculateCost(monthly, price);
130
+ yearlyConsumptionCost = calculator.calculateCost(yearly, price);
131
+ }
132
+
133
+ // 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
+ }
157
+
158
+ const basicChargeAccumulated = basicChargeMonthly * monthsSinceContract;
159
+ const annualFeeAccumulated = (annualFeePerYear / 12) * monthsSinceContract;
160
+ const totalFixCostsAccumulated = basicChargeAccumulated + annualFeeAccumulated;
161
+ const totalYearlyCost = yearlyConsumptionCost + totalFixCostsAccumulated;
162
+
163
+ // Update states
164
+ await this.adapter.setStateAsync(
165
+ `${type}.costs.daily`,
166
+ calculator.roundToDecimals(dailyConsumptionCost, 2),
167
+ true,
168
+ );
169
+ await this.adapter.setStateAsync(
170
+ `${type}.costs.monthly`,
171
+ calculator.roundToDecimals(monthlyConsumptionCost, 2),
172
+ true,
173
+ );
174
+ await this.adapter.setStateAsync(
175
+ `${type}.costs.yearly`,
176
+ calculator.roundToDecimals(yearlyConsumptionCost, 2),
177
+ true,
178
+ );
179
+ await this.adapter.setStateAsync(
180
+ `${type}.costs.totalYearly`,
181
+ calculator.roundToDecimals(totalYearlyCost, 2),
182
+ true,
183
+ );
184
+ await this.adapter.setStateAsync(
185
+ `${type}.costs.annualFee`,
186
+ calculator.roundToDecimals(annualFeeAccumulated, 2),
187
+ true,
188
+ );
189
+ await this.adapter.setStateAsync(
190
+ `${type}.costs.basicCharge`,
191
+ calculator.roundToDecimals(totalFixCostsAccumulated, 2),
192
+ true,
193
+ );
194
+
195
+ // Abschlag
196
+ const abschlagKey = `${configType}Abschlag`;
197
+ const monthlyAbschlag = this.adapter.config[abschlagKey] || 0;
198
+
199
+ if (monthlyAbschlag > 0) {
200
+ const paidTotal = monthlyAbschlag * monthsSinceContract;
201
+ const balance = totalYearlyCost - paidTotal;
202
+ await this.adapter.setStateAsync(`${type}.costs.paidTotal`, calculator.roundToDecimals(paidTotal, 2), true);
203
+ await this.adapter.setStateAsync(`${type}.costs.balance`, calculator.roundToDecimals(balance, 2), true);
204
+ } else {
205
+ await this.adapter.setStateAsync(`${type}.costs.paidTotal`, 0, true);
206
+ await this.adapter.setStateAsync(`${type}.costs.balance`, 0, true);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Closes the billing period and archives data
212
+ *
213
+ * @param {string} type - Utility type
214
+ */
215
+ async closeBillingPeriod(type) {
216
+ this.adapter.log.info(`🔔 Schließe Abrechnungszeitraum für ${type}...`);
217
+
218
+ const endReadingState = await this.adapter.getStateAsync(`${type}.billing.endReading`);
219
+ const endReading = typeof endReadingState?.val === 'number' ? endReadingState.val : null;
220
+
221
+ if (!endReading || endReading <= 0) {
222
+ this.adapter.log.error(
223
+ `❌ Kein gültiger Endzählerstand für ${type}. Bitte trage zuerst einen Wert in ${type}.billing.endReading ein!`,
224
+ );
225
+ await this.adapter.setStateAsync(`${type}.billing.closePeriod`, false, true);
226
+ return;
227
+ }
228
+
229
+ const configType = this.adapter.consumptionManager.getConfigType(type);
230
+ const contractStartKey = `${configType}ContractStart`;
231
+ const contractStart = this.adapter.config[contractStartKey];
232
+
233
+ if (!contractStart) {
234
+ this.adapter.log.error(`❌ Kein Vertragsbeginn für ${type} konfiguriert. Kann Jahr nicht bestimmen.`);
235
+ await this.adapter.setStateAsync(`${type}.billing.closePeriod`, false, true);
236
+ return;
237
+ }
238
+
239
+ const startDate = calculator.parseGermanDate(contractStart);
240
+ if (!startDate) {
241
+ this.adapter.log.error(`❌ Ungültiges Datum-Format für Vertragsbeginn: ${contractStart}`);
242
+ await this.adapter.setStateAsync(`${type}.billing.closePeriod`, false, true);
243
+ return;
244
+ }
245
+
246
+ const year = startDate.getFullYear();
247
+
248
+ // Check if this is a multi-meter setup
249
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
250
+ const isMultiMeter = meters.length > 1;
251
+
252
+ // Archives - use totals for multi-meter, main meter for single meter
253
+ let yearlyState, totalYearlyState, balanceState;
254
+
255
+ if (isMultiMeter) {
256
+ // Multi-meter: use totals
257
+ yearlyState = await this.adapter.getStateAsync(`${type}.totals.consumption.yearly`);
258
+ totalYearlyState = await this.adapter.getStateAsync(`${type}.totals.costs.totalYearly`);
259
+ // Balance is not available in totals, use main meter's balance as representative
260
+ balanceState = await this.adapter.getStateAsync(`${type}.costs.balance`);
261
+ this.adapter.log.info(`Archiving multi-meter totals for ${type} (${meters.length} meters)`);
262
+ } else {
263
+ // Single meter: use main meter values
264
+ yearlyState = await this.adapter.getStateAsync(`${type}.consumption.yearly`);
265
+ totalYearlyState = await this.adapter.getStateAsync(`${type}.costs.totalYearly`);
266
+ balanceState = await this.adapter.getStateAsync(`${type}.costs.balance`);
267
+ }
268
+
269
+ const yearly = yearlyState?.val || 0;
270
+ const totalYearly = totalYearlyState?.val || 0;
271
+ const balance = balanceState?.val || 0;
272
+
273
+ const htNtEnabledKey = `${configType}HtNtEnabled`;
274
+ const htNtEnabled = this.adapter.config[htNtEnabledKey] || false;
275
+
276
+ // ... truncated history creation for brevity, assuming standard implementation ...
277
+ // In reality, I should copy the full logic from main.js but adapt 'this' to 'this.adapter'
278
+ // I will do that now.
279
+
280
+ this.adapter.log.info(`📦 Archiviere Daten für ${type} Jahr ${year}...`);
281
+
282
+ await this.adapter.setObjectNotExistsAsync(`${type}.history`, {
283
+ type: 'channel',
284
+ common: { name: 'Historie' },
285
+ native: {},
286
+ });
287
+ await this.adapter.setObjectNotExistsAsync(`${type}.history.${year}`, {
288
+ type: 'channel',
289
+ common: { name: `Jahr ${year}` },
290
+ native: {},
291
+ });
292
+
293
+ const consumptionUnit = type === 'gas' ? 'kWh' : type === 'water' ? 'm³' : 'kWh';
294
+
295
+ await this.adapter.setObjectNotExistsAsync(`${type}.history.${year}.yearly`, {
296
+ type: 'state',
297
+ common: {
298
+ name: `Jahresverbrauch ${year}`,
299
+ type: 'number',
300
+ role: 'value',
301
+ read: true,
302
+ write: false,
303
+ unit: consumptionUnit,
304
+ },
305
+ native: {},
306
+ });
307
+ await this.adapter.setStateAsync(`${type}.history.${year}.yearly`, yearly, true);
308
+
309
+ if (htNtEnabled) {
310
+ const htNtStates = [
311
+ { id: 'yearlyHT', name: 'Haupttarif (HT)' },
312
+ { id: 'yearlyNT', name: 'Nebentarif (NT)' },
313
+ ];
314
+ for (const htn of htNtStates) {
315
+ await this.adapter.setObjectNotExistsAsync(`${type}.history.${year}.${htn.id}`, {
316
+ type: 'state',
317
+ common: {
318
+ name: `Jahresverbrauch ${year} ${htn.name}`,
319
+ type: 'number',
320
+ role: 'value',
321
+ read: true,
322
+ write: false,
323
+ unit: consumptionUnit,
324
+ },
325
+ native: {},
326
+ });
327
+ }
328
+ const yHT = (await this.adapter.getStateAsync(`${type}.consumption.yearlyHT`))?.val || 0;
329
+ const yNT = (await this.adapter.getStateAsync(`${type}.consumption.yearlyNT`))?.val || 0;
330
+ await this.adapter.setStateAsync(`${type}.history.${year}.yearlyHT`, yHT, true);
331
+ await this.adapter.setStateAsync(`${type}.history.${year}.yearlyNT`, yNT, true);
332
+ }
333
+
334
+ if (type === 'gas' || type === 'water') {
335
+ const yearlyVolume = (await this.adapter.getStateAsync(`${type}.consumption.yearlyVolume`))?.val || 0;
336
+ await this.adapter.setObjectNotExistsAsync(`${type}.history.${year}.yearlyVolume`, {
337
+ type: 'state',
338
+ common: {
339
+ name: `Jahresverbrauch ${year} (m³)`,
340
+ type: 'number',
341
+ role: 'value',
342
+ read: true,
343
+ write: false,
344
+ unit: 'm³',
345
+ },
346
+ native: {},
347
+ });
348
+ await this.adapter.setStateAsync(`${type}.history.${year}.yearlyVolume`, yearlyVolume, true);
349
+ }
350
+
351
+ await this.adapter.setObjectNotExistsAsync(`${type}.history.${year}.totalYearly`, {
352
+ type: 'state',
353
+ common: {
354
+ name: `Gesamtkosten ${year}`,
355
+ type: 'number',
356
+ role: 'value.money',
357
+ read: true,
358
+ write: false,
359
+ unit: '€',
360
+ },
361
+ native: {},
362
+ });
363
+ await this.adapter.setStateAsync(`${type}.history.${year}.totalYearly`, totalYearly, true);
364
+
365
+ await this.adapter.setObjectNotExistsAsync(`${type}.history.${year}.balance`, {
366
+ type: 'state',
367
+ common: {
368
+ name: `Bilanz ${year}`,
369
+ type: 'number',
370
+ role: 'value.money',
371
+ read: true,
372
+ write: false,
373
+ unit: '€',
374
+ },
375
+ native: {},
376
+ });
377
+ await this.adapter.setStateAsync(`${type}.history.${year}.balance`, balance, true);
378
+
379
+ // Reset and Info
380
+ await this.adapter.setStateAsync(`${type}.billing.newInitialReading`, endReading, true);
381
+ await this.adapter.setStateAsync(`${type}.consumption.yearly`, 0, true);
382
+ if (htNtEnabled) {
383
+ await this.adapter.setStateAsync(`${type}.consumption.yearlyHT`, 0, true);
384
+ await this.adapter.setStateAsync(`${type}.consumption.yearlyNT`, 0, true);
385
+ }
386
+ if (type === 'gas') {
387
+ await this.adapter.setStateAsync(`${type}.consumption.yearlyVolume`, 0, true);
388
+ }
389
+ await this.adapter.setStateAsync(`${type}.costs.yearly`, 0, true);
390
+ await this.adapter.setStateAsync(`${type}.costs.totalYearly`, 0, true);
391
+ // NOTE: basicCharge and annualFee are NOT reset - they stay from config!
392
+ // User is responsible for updating config if tariff changes
393
+ await this.adapter.setStateAsync(`${type}.costs.balance`, 0, true);
394
+ await this.adapter.setStateAsync(`${type}.costs.paidTotal`, 0, true);
395
+ await this.adapter.setStateAsync(`${type}.billing.closePeriod`, false, true);
396
+ await this.adapter.setStateAsync(`${type}.billing.notificationSent`, false, true);
397
+ await this.adapter.setStateAsync(`${type}.billing.notificationChangeSent`, false, true);
398
+
399
+ // Update lastYearStart to the contract anniversary date (NOT Date.now()!)
400
+ // This ensures the next automatic reset happens on the contract date,
401
+ // even if the user closes the period early (e.g. 2 days before)
402
+ const thisYearAnniversary = new Date(startDate);
403
+ thisYearAnniversary.setFullYear(new Date().getFullYear());
404
+ await this.adapter.setStateAsync(`${type}.statistics.lastYearStart`, thisYearAnniversary.getTime(), true);
405
+
406
+ this.adapter.log.info(`✅ Abrechnungszeitraum ${year} für ${type} erfolgreich abgeschlossen!`);
407
+ this.adapter.log.info(
408
+ `💡 Tipp: Prüfe deine Adapter-Konfiguration! Hat sich dein Tarif, Abschlag oder die Grundgebühr geändert?`,
409
+ );
410
+ }
411
+
412
+ /**
413
+ * Closes the billing period for a specific meter (main or additional)
414
+ *
415
+ * @param {string} type - Utility type
416
+ * @param {object} meter - Meter object from multiMeterManager
417
+ */
418
+ async closeBillingPeriodForMeter(type, meter) {
419
+ const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
420
+ const label = meter.displayName || meter.name;
421
+
422
+ this.adapter.log.info(`🔔 Schließe Abrechnungszeitraum für ${basePath} (${label})...`);
423
+
424
+ const endReadingState = await this.adapter.getStateAsync(`${basePath}.billing.endReading`);
425
+ const endReading = typeof endReadingState?.val === 'number' ? endReadingState.val : null;
426
+
427
+ if (!endReading || endReading <= 0) {
428
+ this.adapter.log.error(
429
+ `❌ Kein gültiger Endzählerstand für ${basePath}. Bitte trage zuerst einen Wert ein!`,
430
+ );
431
+ await this.adapter.setStateAsync(`${basePath}.billing.closePeriod`, false, true);
432
+ return;
433
+ }
434
+
435
+ // Get contract date for THIS meter
436
+ let contractStartDate;
437
+ if (meter.name === 'main') {
438
+ const configType = this.adapter.consumptionManager.getConfigType(type);
439
+ contractStartDate = this.adapter.config[`${configType}ContractStart`];
440
+ } else {
441
+ contractStartDate = meter.config?.vertragsbeginn;
442
+ }
443
+
444
+ if (!contractStartDate) {
445
+ this.adapter.log.error(`❌ Kein Vertragsbeginn für ${basePath} konfiguriert.`);
446
+ await this.adapter.setStateAsync(`${basePath}.billing.closePeriod`, false, true);
447
+ return;
448
+ }
449
+
450
+ const startDate = calculator.parseGermanDate(contractStartDate);
451
+ if (!startDate) {
452
+ this.adapter.log.error(`❌ Ungültiges Datum-Format für Vertragsbeginn: ${contractStartDate}`);
453
+ await this.adapter.setStateAsync(`${basePath}.billing.closePeriod`, false, true);
454
+ return;
455
+ }
456
+
457
+ const year = startDate.getFullYear();
458
+
459
+ // Archive data for this meter
460
+ // TODO: Implement full history archiving for individual meters
461
+ // For now, just reset the meter
462
+
463
+ // Reset consumption and costs for this meter
464
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, 0, true);
465
+ if (type === 'gas') {
466
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, 0, true);
467
+ }
468
+ await this.adapter.setStateAsync(`${basePath}.costs.yearly`, 0, true);
469
+ await this.adapter.setStateAsync(`${basePath}.costs.totalYearly`, 0, true);
470
+ await this.adapter.setStateAsync(`${basePath}.costs.balance`, 0, true);
471
+ await this.adapter.setStateAsync(`${basePath}.costs.paidTotal`, 0, true);
472
+ await this.adapter.setStateAsync(`${basePath}.billing.closePeriod`, false, true);
473
+ await this.adapter.setStateAsync(`${basePath}.billing.notificationSent`, false, true);
474
+ await this.adapter.setStateAsync(`${basePath}.billing.notificationChangeSent`, false, true);
475
+
476
+ // Update lastYearStart to contract anniversary
477
+ const thisYearAnniversary = new Date(startDate);
478
+ thisYearAnniversary.setFullYear(new Date().getFullYear());
479
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, thisYearAnniversary.getTime(), true);
480
+
481
+ // Update totals if multiple meters exist
482
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
483
+ if (meters.length > 1) {
484
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
485
+ }
486
+
487
+ this.adapter.log.info(`✅ Abrechnungszeitraum ${year} für ${basePath} erfolgreich abgeschlossen!`);
488
+ this.adapter.log.info(
489
+ `💡 Tipp: Prüfe deine Adapter-Konfiguration! Hat sich dein Tarif, Abschlag oder die Grundgebühr geändert?`,
490
+ );
491
+ }
492
+
493
+ /**
494
+ * Updates billing countdown
495
+ *
496
+ * @param {string} type - Utility type
497
+ */
498
+ async updateBillingCountdown(type) {
499
+ const configType = this.adapter.consumptionManager.getConfigType(type);
500
+ const contractStart = this.adapter.config[`${configType}ContractStart`];
501
+
502
+ if (!contractStart) {
503
+ return;
504
+ }
505
+
506
+ const startDate = calculator.parseGermanDate(contractStart);
507
+ if (!startDate) {
508
+ return;
509
+ }
510
+
511
+ const today = new Date();
512
+ const nextAnniversary = new Date(startDate);
513
+ nextAnniversary.setFullYear(today.getFullYear());
514
+
515
+ if (nextAnniversary < today) {
516
+ nextAnniversary.setFullYear(today.getFullYear() + 1);
517
+ }
518
+
519
+ const msPerDay = 1000 * 60 * 60 * 24;
520
+ const daysRemaining = Math.ceil((nextAnniversary.getTime() - today.getTime()) / msPerDay);
521
+ const displayPeriodEnd = new Date(nextAnniversary);
522
+ displayPeriodEnd.setDate(displayPeriodEnd.getDate() - 1);
523
+
524
+ await this.adapter.setStateAsync(`${type}.billing.daysRemaining`, daysRemaining, true);
525
+ await this.adapter.setStateAsync(
526
+ `${type}.billing.periodEnd`,
527
+ displayPeriodEnd.toLocaleDateString('de-DE'),
528
+ true,
529
+ );
530
+ }
531
+
532
+ /**
533
+ * Checks if any period resets are needed
534
+ */
535
+ async checkPeriodResets() {
536
+ if (typeof this.adapter.messagingHandler?.checkNotifications === 'function') {
537
+ await this.adapter.messagingHandler.checkNotifications();
538
+ }
539
+
540
+ const now = new Date();
541
+ const types = ['gas', 'water', 'electricity', 'pv'];
542
+
543
+ for (const type of types) {
544
+ const configType = this.adapter.consumptionManager.getConfigType(type);
545
+ if (!this.adapter.config[`${configType}Aktiv`]) {
546
+ continue;
547
+ }
548
+
549
+ // Update current price and tariff (e.g. for switching HT/NT)
550
+ if (this.adapter.consumptionManager) {
551
+ await this.adapter.consumptionManager.updateCurrentPrice(type);
552
+ }
553
+
554
+ const nowDate = new Date(now);
555
+
556
+ // DAILY RESET: All meters reset together at midnight
557
+ const lastDayStart = await this.adapter.getStateAsync(`${type}.statistics.lastDayStart`);
558
+ if (lastDayStart?.val) {
559
+ const lastDay = new Date(lastDayStart.val);
560
+ if (nowDate.getDate() !== lastDay.getDate()) {
561
+ await this.resetDailyCounters(type);
562
+ }
563
+ }
564
+
565
+ // MONTHLY RESET: All meters reset together on 1st of month
566
+ const lastMonthStart = await this.adapter.getStateAsync(`${type}.statistics.lastMonthStart`);
567
+ if (lastMonthStart?.val) {
568
+ const lastMonth = new Date(lastMonthStart.val);
569
+ if (nowDate.getMonth() !== lastMonth.getMonth()) {
570
+ await this.resetMonthlyCounters(type);
571
+ }
572
+ }
573
+
574
+ // YEARLY RESET: Each meter resets individually based on ITS contract date
575
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
576
+ for (const meter of meters) {
577
+ const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
578
+ const lastYearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
579
+
580
+ if (lastYearStartState?.val) {
581
+ const lastYearStartDate = new Date(lastYearStartState.val);
582
+
583
+ // Get contract date for THIS specific meter
584
+ let contractStartDate;
585
+ if (meter.name === 'main') {
586
+ // Main meter: use adapter config
587
+ contractStartDate = this.adapter.config[`${configType}ContractStart`];
588
+ } else {
589
+ // Additional meter: use meter's individual config
590
+ contractStartDate = meter.config?.vertragsbeginn;
591
+ }
592
+
593
+ if (contractStartDate) {
594
+ const contractStart = calculator.parseGermanDate(contractStartDate);
595
+ if (contractStart) {
596
+ const annMonth = contractStart.getMonth();
597
+ const annDay = contractStart.getDate();
598
+ const isPast =
599
+ nowDate.getMonth() > annMonth ||
600
+ (nowDate.getMonth() === annMonth && nowDate.getDate() >= annDay);
601
+
602
+ if (isPast && lastYearStartDate.getFullYear() !== nowDate.getFullYear()) {
603
+ this.adapter.log.info(
604
+ `Yearly reset for ${basePath} (contract anniversary: ${contractStartDate})`,
605
+ );
606
+ await this.resetYearlyCountersForMeter(type, meter);
607
+
608
+ // Update totals if multiple meters exist
609
+ if (meters.length > 1) {
610
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
611
+ }
612
+ }
613
+ }
614
+ } else if (nowDate.getFullYear() !== lastYearStartDate.getFullYear()) {
615
+ // No contract date: reset on January 1st
616
+ this.adapter.log.info(`Yearly reset for ${basePath} (calendar year)`);
617
+ await this.resetYearlyCountersForMeter(type, meter);
618
+
619
+ // Update totals if multiple meters exist
620
+ if (meters.length > 1) {
621
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
622
+ }
623
+ }
624
+ }
625
+ }
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Resets daily counters
631
+ *
632
+ * @param {string} type - Utility type
633
+ */
634
+ async resetDailyCounters(type) {
635
+ this.adapter.log.info(`Resetting daily counters for ${type}`);
636
+
637
+ // Get all meters for this type (main + additional meters)
638
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
639
+
640
+ if (meters.length === 0) {
641
+ this.adapter.log.warn(`No meters found for ${type}, skipping daily reset`);
642
+ return;
643
+ }
644
+
645
+ // Reset each meter
646
+ for (const meter of meters) {
647
+ const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
648
+ const label = meter.displayName || meter.name;
649
+
650
+ this.adapter.log.debug(`Resetting daily counter for ${basePath} (${label})`);
651
+
652
+ const dailyState = await this.adapter.getStateAsync(`${basePath}.consumption.daily`);
653
+ const dailyValue = dailyState?.val || 0;
654
+
655
+ // Save last day consumption
656
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastDay`, dailyValue, true);
657
+
658
+ await this.adapter.setStateAsync(`${basePath}.consumption.daily`, 0, true);
659
+
660
+ if (type === 'gas') {
661
+ const dailyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.dailyVolume`);
662
+ const dailyVolumeValue = dailyVolume?.val || 0;
663
+ // Save last day volume for gas
664
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastDayVolume`, dailyVolumeValue, true);
665
+ await this.adapter.setStateAsync(`${basePath}.consumption.dailyVolume`, 0, true);
666
+ }
667
+
668
+ await this.adapter.setStateAsync(`${basePath}.costs.daily`, 0, true);
669
+
670
+ // Update lastDayStart timestamp
671
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastDayStart`, Date.now(), true);
672
+
673
+ await this.adapter.setStateAsync(
674
+ `${basePath}.statistics.averageDaily`,
675
+ calculator.roundToDecimals(dailyValue, 2),
676
+ true,
677
+ );
678
+ }
679
+
680
+ // Update totals if multiple meters exist
681
+ if (meters.length > 1) {
682
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Resets monthly counters
688
+ *
689
+ * @param {string} type - Utility type
690
+ */
691
+ async resetMonthlyCounters(type) {
692
+ this.adapter.log.info(`Resetting monthly counters for ${type}`);
693
+
694
+ // Get all meters for this type (main + additional meters)
695
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
696
+
697
+ if (meters.length === 0) {
698
+ this.adapter.log.warn(`No meters found for ${type}, skipping monthly reset`);
699
+ return;
700
+ }
701
+
702
+ // Reset each meter
703
+ for (const meter of meters) {
704
+ const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
705
+ const label = meter.displayName || meter.name;
706
+
707
+ this.adapter.log.debug(`Resetting monthly counter for ${basePath} (${label})`);
708
+
709
+ const monthlyState = await this.adapter.getStateAsync(`${basePath}.consumption.monthly`);
710
+ const monthlyValue = monthlyState?.val || 0;
711
+
712
+ await this.adapter.setStateAsync(`${basePath}.consumption.monthly`, 0, true);
713
+
714
+ if (type === 'gas') {
715
+ await this.adapter.setStateAsync(`${basePath}.consumption.monthlyVolume`, 0, true);
716
+ }
717
+
718
+ await this.adapter.setStateAsync(`${basePath}.costs.monthly`, 0, true);
719
+
720
+ // Update lastMonthStart timestamp
721
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastMonthStart`, Date.now(), true);
722
+
723
+ await this.adapter.setStateAsync(
724
+ `${basePath}.statistics.averageMonthly`,
725
+ calculator.roundToDecimals(monthlyValue, 2),
726
+ true,
727
+ );
728
+ }
729
+
730
+ // Update totals if multiple meters exist
731
+ if (meters.length > 1) {
732
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Resets yearly counters
738
+ *
739
+ * @param {string} type - Utility type
740
+ */
741
+ async resetYearlyCounters(type) {
742
+ this.adapter.log.info(`Resetting yearly counters for ${type}`);
743
+
744
+ // Get all meters for this type (main + additional meters)
745
+ const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
746
+
747
+ if (meters.length === 0) {
748
+ this.adapter.log.warn(`No meters found for ${type}, skipping yearly reset`);
749
+ return;
750
+ }
751
+
752
+ // Reset each meter
753
+ for (const meter of meters) {
754
+ const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
755
+ const label = meter.displayName || meter.name;
756
+
757
+ this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
758
+
759
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, 0, true);
760
+
761
+ if (type === 'gas') {
762
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, 0, true);
763
+ }
764
+
765
+ await this.adapter.setStateAsync(`${basePath}.costs.yearly`, 0, true);
766
+ await this.adapter.setStateAsync(`${basePath}.billing.notificationSent`, false, true);
767
+ await this.adapter.setStateAsync(`${basePath}.billing.notificationChangeSent`, false, true);
768
+
769
+ // Update lastYearStart timestamp
770
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, Date.now(), true);
771
+ }
772
+
773
+ // Update totals if multiple meters exist
774
+ if (meters.length > 1) {
775
+ await this.adapter.multiMeterManager.updateTotalCosts(type);
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Resets yearly counters for a SINGLE meter (used for individual contract anniversaries)
781
+ *
782
+ * @param {string} type - Utility type
783
+ * @param {object} meter - Meter object from multiMeterManager
784
+ */
785
+ async resetYearlyCountersForMeter(type, meter) {
786
+ const basePath = meter.name === 'main' ? type : `${type}.${meter.name}`;
787
+ const label = meter.displayName || meter.name;
788
+
789
+ this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
790
+
791
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, 0, true);
792
+
793
+ if (type === 'gas') {
794
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, 0, true);
795
+ }
796
+
797
+ await this.adapter.setStateAsync(`${basePath}.costs.yearly`, 0, true);
798
+ await this.adapter.setStateAsync(`${basePath}.billing.notificationSent`, false, true);
799
+ await this.adapter.setStateAsync(`${basePath}.billing.notificationChangeSent`, false, true);
800
+
801
+ // Update lastYearStart timestamp
802
+ await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, Date.now(), true);
803
+ }
804
+ }
805
+
806
+ module.exports = BillingManager;