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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/admin/i18n/de.json +5 -0
- package/admin/i18n/en.json +5 -0
- package/admin/i18n/es.json +5 -0
- package/admin/i18n/fr.json +5 -0
- package/admin/i18n/it.json +5 -0
- package/admin/i18n/nl.json +5 -0
- package/admin/i18n/pl.json +5 -0
- package/admin/i18n/pt.json +5 -0
- package/admin/i18n/ru.json +5 -0
- package/admin/i18n/uk.json +5 -0
- package/admin/i18n/zh-cn.json +5 -0
- package/admin/jsonConfig.json +1542 -0
- package/admin/utility-monitor.png +0 -0
- package/io-package.json +188 -0
- package/lib/adapter-config.d.ts +19 -0
- package/lib/billingManager.js +806 -0
- package/lib/calculator.js +254 -0
- package/lib/configParser.js +92 -0
- package/lib/consumptionManager.js +407 -0
- package/lib/messagingHandler.js +339 -0
- package/lib/multiMeterManager.js +749 -0
- package/lib/stateManager.js +1556 -0
- package/main.js +297 -0
- package/package.json +80 -0
|
@@ -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;
|