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.
Files changed (36) hide show
  1. package/README.md +110 -62
  2. package/admin/custom/.vite/manifest.json +90 -0
  3. package/admin/custom/@mf-types/Components.d.ts +2 -0
  4. package/admin/custom/@mf-types/compiled-types/Components/CSVImporter.d.ts +11 -0
  5. package/admin/custom/@mf-types/compiled-types/Components.d.ts +2 -0
  6. package/admin/custom/@mf-types.d.ts +3 -0
  7. package/admin/custom/@mf-types.zip +0 -0
  8. package/admin/custom/CSVImporter_v15_11.js +4415 -0
  9. package/admin/custom/assets/Components-i0AZ59nl.js +18887 -0
  10. package/admin/custom/assets/UtilityMonitor__loadShare__react__loadShare__-Da99Mak4.js +42 -0
  11. package/admin/custom/assets/UtilityMonitor__mf_v__runtimeInit__mf_v__-BmC4OGk6.js +16 -0
  12. package/admin/custom/assets/_commonjsHelpers-Dj2_voLF.js +30 -0
  13. package/admin/custom/assets/hostInit-DEXfeB0W.js +10 -0
  14. package/admin/custom/assets/index-B3WVNJTz.js +401 -0
  15. package/admin/custom/assets/index-VBwl8x_k.js +64 -0
  16. package/admin/custom/assets/preload-helper-BelkbqnE.js +61 -0
  17. package/admin/custom/assets/virtualExposes-CqCLUNLT.js +19 -0
  18. package/admin/custom/index.html +12 -0
  19. package/admin/custom/mf-manifest.json +1 -0
  20. package/admin/jsonConfig.json +90 -31
  21. package/io-package.json +15 -31
  22. package/lib/billingManager.js +382 -137
  23. package/lib/calculator.js +41 -146
  24. package/lib/consumptionManager.js +9 -252
  25. package/lib/importManager.js +300 -0
  26. package/lib/messagingHandler.js +4 -2
  27. package/lib/meter/MeterRegistry.js +110 -0
  28. package/lib/multiMeterManager.js +580 -173
  29. package/lib/stateManager.js +502 -31
  30. package/lib/utils/billingHelper.js +69 -0
  31. package/lib/utils/consumptionHelper.js +47 -0
  32. package/lib/utils/helpers.js +234 -0
  33. package/lib/utils/stateCache.js +147 -0
  34. package/lib/utils/typeMapper.js +19 -0
  35. package/main.js +67 -8
  36. package/package.json +10 -4
@@ -3,6 +3,14 @@
3
3
  const calculator = require('./calculator');
4
4
  const stateManager = require('./stateManager');
5
5
  const { parseConfigNumber } = require('./configParser');
6
+ const { getConfigType } = require('./utils/typeMapper');
7
+ const MeterRegistry = require('./meter/MeterRegistry');
8
+ const helpers = require('./utils/helpers');
9
+ const consumptionHelper = require('./utils/consumptionHelper');
10
+ const billingHelper = require('./utils/billingHelper');
11
+
12
+ // Default constants
13
+ const DEFAULT_SPIKE_THRESHOLD = 500; // Default sensor spike detection limit
6
14
 
7
15
  /**
8
16
  * MultiMeterManager handles multiple meters per utility type
@@ -19,41 +27,19 @@ class MultiMeterManager {
19
27
  this.consumptionManager = consumptionManager;
20
28
  this.billingManager = billingManager;
21
29
  this.lastSensorValues = {};
22
- this.meterRegistry = {}; // Maps sensorDP → {type, meterName}
30
+ this.meterRegistry = new MeterRegistry();
31
+ this.tempBaselineStore = {}; // Tracks first value after start to prevent peaks
23
32
  }
24
33
 
25
34
  /**
26
35
  * Maps internal utility type to config/state name
27
36
  *
37
+ * @deprecated Use getConfigType from utils/typeMapper directly
28
38
  * @param {string} type - gas, water, or electricity
29
39
  * @returns {string} - gas, wasser, or strom
30
40
  */
31
41
  getConfigType(type) {
32
- const mapping = {
33
- electricity: 'strom',
34
- water: 'wasser',
35
- gas: 'gas',
36
- };
37
- return mapping[type] || type;
38
- }
39
-
40
- /**
41
- * Normalizes meter name to valid folder name
42
- * Rules: lowercase, alphanumeric only, max 20 chars
43
- *
44
- * @param {string} name - User-provided meter name
45
- * @returns {string} - Normalized name
46
- */
47
- normalizeMeterName(name) {
48
- if (!name || typeof name !== 'string') {
49
- return 'unnamed';
50
- }
51
- return (
52
- name
53
- .toLowerCase()
54
- .replace(/[^a-z0-9]/g, '')
55
- .substring(0, 20) || 'unnamed'
56
- );
42
+ return getConfigType(type);
57
43
  }
58
44
 
59
45
  /**
@@ -72,7 +58,7 @@ class MultiMeterManager {
72
58
  if (mainActive) {
73
59
  // Get main meter name from config and normalize
74
60
  const mainMeterName = this.adapter.config[`${configType}MainMeterName`] || 'main';
75
- const normalizedName = this.normalizeMeterName(mainMeterName);
61
+ const normalizedName = helpers.normalizeMeterName(mainMeterName);
76
62
  const displayName = mainMeterName; // Original name for display
77
63
 
78
64
  meters.push({
@@ -97,7 +83,7 @@ class MultiMeterManager {
97
83
  if (Array.isArray(additionalMeters)) {
98
84
  for (const meterConfig of additionalMeters) {
99
85
  if (meterConfig && meterConfig.name && meterConfig.sensorDP) {
100
- const normalizedName = this.normalizeMeterName(meterConfig.name);
86
+ const normalizedName = helpers.normalizeMeterName(meterConfig.name);
101
87
 
102
88
  // Debug: Log raw config for troubleshooting
103
89
  this.adapter.log.debug(
@@ -129,13 +115,13 @@ class MultiMeterManager {
129
115
  }
130
116
 
131
117
  /**
132
- * Finds meter by sensor datapoint
118
+ * Finds meters by sensor datapoint
133
119
  *
134
120
  * @param {string} sensorDP - Sensor datapoint ID
135
- * @returns {object|null} - {type, meterName} or null
121
+ * @returns {Array} - Array of {type, meterName} objects
136
122
  */
137
123
  findMeterBySensor(sensorDP) {
138
- return this.meterRegistry[sensorDP] || null;
124
+ return this.meterRegistry.findBySensor(sensorDP);
139
125
  }
140
126
 
141
127
  /**
@@ -186,15 +172,24 @@ class MultiMeterManager {
186
172
 
187
173
  if (!config.sensorDP) {
188
174
  this.adapter.log.warn(`${type} meter "${label}" has no sensor datapoint configured!`);
189
- await this.adapter.setStateAsync(`${basePath}.info.sensorActive`, false, true);
175
+ try {
176
+ await this.adapter.setStateAsync(`${basePath}.info.sensorActive`, false, true);
177
+ } catch (error) {
178
+ this.adapter.log.warn(`Could not set sensorActive state for ${basePath}: ${error.message}`);
179
+ }
190
180
  return;
191
181
  }
192
182
 
193
- // Create state structure
194
- await stateManager.createMeterStructure(this.adapter, type, meterName, config);
183
+ // Create state structure with error handling
184
+ try {
185
+ await stateManager.createMeterStructure(this.adapter, type, meterName, config);
186
+ } catch (error) {
187
+ this.adapter.log.error(`Failed to create state structure for ${basePath}: ${error.message}`);
188
+ return;
189
+ }
195
190
 
196
- // Register sensor in registry
197
- this.meterRegistry[config.sensorDP] = { type, meterName };
191
+ // Register sensor mapping
192
+ this.meterRegistry.register(config.sensorDP, type, meterName);
198
193
 
199
194
  this.adapter.log.debug(`Using sensor datapoint for ${type}.${meterName}: ${config.sensorDP}`);
200
195
 
@@ -218,8 +213,10 @@ class MultiMeterManager {
218
213
  // Initialize with current sensor value
219
214
  try {
220
215
  const sensorState = await this.adapter.getForeignStateAsync(config.sensorDP);
221
- if (sensorState && sensorState.val !== null && typeof sensorState.val === 'number') {
222
- await this.handleSensorUpdate(type, meterName, config.sensorDP, sensorState.val);
216
+ if (sensorState && sensorState.val != null) {
217
+ // Convert to number (handles strings, German commas, etc.)
218
+ const numValue = calculator.ensureNumber(sensorState.val);
219
+ await this.handleSensorUpdate(type, meterName, config.sensorDP, numValue);
223
220
  }
224
221
  } catch (error) {
225
222
  this.adapter.log.warn(`Could not read initial value from ${config.sensorDP}: ${error.message}`);
@@ -227,7 +224,7 @@ class MultiMeterManager {
227
224
  }
228
225
 
229
226
  // Initialize period start timestamps
230
- const timestampRoles = ['lastDayStart', 'lastMonthStart', 'lastYearStart'];
227
+ const timestampRoles = ['lastDayStart', 'lastWeekStart', 'lastMonthStart', 'lastYearStart'];
231
228
 
232
229
  for (const role of timestampRoles) {
233
230
  const statePath = `${basePath}.statistics.${role}`;
@@ -270,37 +267,9 @@ class MultiMeterManager {
270
267
  }
271
268
  }
272
269
 
273
- // Initialize yearly consumption from initial reading if set
274
- if (config.initialReading > 0) {
275
- const sensorState = await this.adapter.getForeignStateAsync(config.sensorDP);
276
- if (sensorState && typeof sensorState.val === 'number') {
277
- let currentRaw = sensorState.val;
278
-
279
- if (config.offset !== 0) {
280
- currentRaw = currentRaw - config.offset;
281
- this.adapter.log.debug(
282
- `Applied offset for ${type}.${meterName}: -${config.offset}, new value: ${currentRaw}`,
283
- );
284
- }
285
- let yearlyConsumption = Math.max(0, currentRaw - config.initialReading);
286
-
287
- // For gas: convert m³ to kWh
288
- if (type === 'gas') {
289
- const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
290
- const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
291
- const yearlyVolume = yearlyConsumption;
292
- yearlyConsumption = calculator.convertGasM3ToKWh(yearlyConsumption, brennwert, zZahl);
293
- await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, yearlyVolume, true);
294
- this.adapter.log.info(
295
- `Init yearly ${type}.${meterName}: ${yearlyConsumption.toFixed(2)} kWh = ${(currentRaw - config.initialReading).toFixed(2)} m³`,
296
- );
297
- } else {
298
- this.adapter.log.info(`Init yearly ${type}.${meterName}: ${yearlyConsumption.toFixed(2)}`);
299
- }
300
-
301
- await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyConsumption, true);
302
- }
303
- }
270
+ // NOTE: Initial yearly consumption is calculated in handleSensorUpdate()
271
+ // which is called immediately after this initialization (line 224)
272
+ // This avoids race conditions where state objects aren't fully created yet
304
273
 
305
274
  // Update current price
306
275
  await this.updateCurrentPrice(type, meterName, config);
@@ -338,9 +307,144 @@ class MultiMeterManager {
338
307
  // Initial cost calculation
339
308
  await this.updateCosts(type, meterName, config);
340
309
 
310
+ // Reconstruct weekly consumption from daily values if needed
311
+ await this.reconstructPeriodConsumption(type, meterName, basePath);
312
+
341
313
  this.adapter.log.debug(`Meter initialization completed for ${type}.${meterName}`);
342
314
  }
343
315
 
316
+ /**
317
+ * Reconstructs weekly and monthly consumption values after adapter restart
318
+ * This fixes data loss when adapter was offline and missed delta accumulation
319
+ *
320
+ * @param {string} type - Utility type
321
+ * @param {string} meterName - Meter name
322
+ * @param {string} basePath - State base path
323
+ */
324
+ async reconstructPeriodConsumption(type, meterName, basePath) {
325
+ const now = Date.now();
326
+
327
+ // Get period start timestamps
328
+ const lastWeekStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastWeekStart`);
329
+ const lastDayStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastDayStart`);
330
+
331
+ if (!lastWeekStartState?.val || !lastDayStartState?.val) {
332
+ this.adapter.log.debug(`[${basePath}] No period timestamps found, skipping reconstruction`);
333
+ return;
334
+ }
335
+
336
+ const lastWeekStart = lastWeekStartState.val;
337
+ const lastDayStart = lastDayStartState.val;
338
+
339
+ // Calculate days since week start
340
+ const daysSinceWeekStart = (now - lastWeekStart) / (24 * 60 * 60 * 1000);
341
+
342
+ // Only reconstruct if we're within a valid week (0-7 days)
343
+ if (daysSinceWeekStart < 0 || daysSinceWeekStart > 7) {
344
+ this.adapter.log.debug(
345
+ `[${basePath}] Week period out of range (${daysSinceWeekStart.toFixed(1)} days), skipping reconstruction`,
346
+ );
347
+ return;
348
+ }
349
+
350
+ // Get current consumption values
351
+ const weeklyState = await this.adapter.getStateAsync(`${basePath}.consumption.weekly`);
352
+ const dailyState = await this.adapter.getStateAsync(`${basePath}.consumption.daily`);
353
+ const lastDayState = await this.adapter.getStateAsync(`${basePath}.statistics.lastDay`);
354
+
355
+ const currentWeekly = weeklyState?.val || 0;
356
+ const currentDaily = dailyState?.val || 0;
357
+ const lastDay = lastDayState?.val || 0;
358
+
359
+ // Calculate expected weekly based on lastDay values accumulated since lastWeekStart
360
+ // Simple approach: If daily counter was reset today and we have lastDay,
361
+ // weekly should be at least lastDay + currentDaily
362
+ const daysSinceDayReset = (now - lastDayStart) / (24 * 60 * 60 * 1000);
363
+
364
+ // If daily was reset (daysSinceDayReset < 1) and weekly seems too low
365
+ if (daysSinceDayReset < 1 && currentWeekly < lastDay + currentDaily) {
366
+ // Weekly might have missed the lastDay value
367
+ // This can happen if adapter restarted after daily reset
368
+ const reconstructedWeekly = calculator.roundToDecimals(currentWeekly + lastDay, 2);
369
+
370
+ if (reconstructedWeekly > currentWeekly) {
371
+ this.adapter.log.info(
372
+ `[${basePath}] Reconstructing weekly: ${currentWeekly} -> ${reconstructedWeekly} (added lastDay: ${lastDay})`,
373
+ );
374
+ await this.adapter.setStateAsync(`${basePath}.consumption.weekly`, reconstructedWeekly, true);
375
+
376
+ // Also reconstruct gas volume if applicable
377
+ if (type === 'gas') {
378
+ const weeklyVolumeState = await this.adapter.getStateAsync(`${basePath}.consumption.weeklyVolume`);
379
+ const lastDayVolumeState = await this.adapter.getStateAsync(`${basePath}.statistics.lastDayVolume`);
380
+ const currentWeeklyVolume = weeklyVolumeState?.val || 0;
381
+ const lastDayVolume = lastDayVolumeState?.val || 0;
382
+
383
+ if (lastDayVolume > 0) {
384
+ const reconstructedWeeklyVolume = calculator.roundToDecimals(
385
+ currentWeeklyVolume + lastDayVolume,
386
+ 4,
387
+ );
388
+ await this.adapter.setStateAsync(
389
+ `${basePath}.consumption.weeklyVolume`,
390
+ reconstructedWeeklyVolume,
391
+ true,
392
+ );
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ // Similar logic for monthly reconstruction
399
+ const lastMonthStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastMonthStart`);
400
+ if (lastMonthStartState?.val) {
401
+ const lastMonthStart = lastMonthStartState.val;
402
+ const daysSinceMonthStart = (now - lastMonthStart) / (24 * 60 * 60 * 1000);
403
+
404
+ // Only if within valid month range (0-31 days)
405
+ if (daysSinceMonthStart >= 0 && daysSinceMonthStart <= 31) {
406
+ const monthlyState = await this.adapter.getStateAsync(`${basePath}.consumption.monthly`);
407
+ const currentMonthly = monthlyState?.val || 0;
408
+
409
+ // If daily was reset and monthly seems to be missing the lastDay
410
+ if (daysSinceDayReset < 1 && currentMonthly < lastDay + currentDaily) {
411
+ const reconstructedMonthly = calculator.roundToDecimals(currentMonthly + lastDay, 2);
412
+
413
+ if (reconstructedMonthly > currentMonthly) {
414
+ this.adapter.log.info(
415
+ `[${basePath}] Reconstructing monthly: ${currentMonthly} -> ${reconstructedMonthly} (added lastDay: ${lastDay})`,
416
+ );
417
+ await this.adapter.setStateAsync(`${basePath}.consumption.monthly`, reconstructedMonthly, true);
418
+
419
+ // Also reconstruct gas volume if applicable
420
+ if (type === 'gas') {
421
+ const monthlyVolumeState = await this.adapter.getStateAsync(
422
+ `${basePath}.consumption.monthlyVolume`,
423
+ );
424
+ const lastDayVolumeState = await this.adapter.getStateAsync(
425
+ `${basePath}.statistics.lastDayVolume`,
426
+ );
427
+ const currentMonthlyVolume = monthlyVolumeState?.val || 0;
428
+ const lastDayVolume = lastDayVolumeState?.val || 0;
429
+
430
+ if (lastDayVolume > 0) {
431
+ const reconstructedMonthlyVolume = calculator.roundToDecimals(
432
+ currentMonthlyVolume + lastDayVolume,
433
+ 4,
434
+ );
435
+ await this.adapter.setStateAsync(
436
+ `${basePath}.consumption.monthlyVolume`,
437
+ reconstructedMonthlyVolume,
438
+ true,
439
+ );
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
447
+
344
448
  /**
345
449
  * Handles sensor value updates
346
450
  *
@@ -356,7 +460,9 @@ class MultiMeterManager {
356
460
  }
357
461
 
358
462
  const basePath = `${type}.${meterName}`;
359
- this.adapter.log.debug(`Sensor update for ${basePath}: ${value}`);
463
+ this.adapter.log.debug(`[${basePath}] handleSensorUpdate: value=${value}, sensorDP=${sensorDP}`);
464
+
465
+ const now = Date.now();
360
466
 
361
467
  // Get meter config
362
468
  const meters = this.getMetersForType(type);
@@ -367,92 +473,328 @@ class MultiMeterManager {
367
473
  }
368
474
 
369
475
  const config = meter.config;
370
- const now = Date.now();
476
+
477
+ // Pre-process consumption value (offset, gas conversion)
478
+ const processed = await this._preprocessValue(type, value, config);
479
+ const { consumption, consumptionM3 } = processed;
480
+
481
+ // 1. Initialization Logic (Per Session)
482
+ if (this.lastSensorValues[sensorDP] === undefined) {
483
+ await this._handleFirstSensorValue(type, meterName, sensorDP, processed, basePath, config, now);
484
+ return;
485
+ }
486
+
487
+ // 2. Update meter reading states
488
+ await this.adapter.setStateAsync(`${basePath}.info.meterReading`, consumption, true);
489
+ if (type === 'gas') {
490
+ await this.adapter.setStateAsync(`${basePath}.info.meterReadingVolume`, consumptionM3 || 0, true);
491
+ }
492
+
493
+ // 3. Delta Calibration & Spike Protection
494
+ const lastValue = this.lastSensorValues[sensorDP];
495
+ this.lastSensorValues[sensorDP] = consumption;
496
+
497
+ if (consumption < lastValue) {
498
+ await this._handleMeterReset(type, meterName, lastValue, consumption, config);
499
+ return;
500
+ }
501
+
502
+ const delta = calculator.roundToDecimals(consumption - lastValue, 4);
503
+ if (delta <= 0) {
504
+ return;
505
+ }
506
+
507
+ const spikeThreshold = this.adapter.config.sensorSpikeThreshold || DEFAULT_SPIKE_THRESHOLD;
508
+ if (delta > spikeThreshold) {
509
+ await this._handleSuspiciousDelta(
510
+ type,
511
+ meterName,
512
+ delta,
513
+ consumption,
514
+ consumptionM3,
515
+ config,
516
+ basePath,
517
+ now,
518
+ );
519
+ return;
520
+ }
521
+
522
+ this.adapter.log.debug(`${type}.${meterName} delta: ${delta}`);
523
+
524
+ // 4. Update Consumption Values (Daily, Weekly, Monthly)
525
+ const deltaVolume =
526
+ type === 'gas'
527
+ ? delta /
528
+ ((this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT) *
529
+ (this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL))
530
+ : 0;
531
+ await this._updateTotalConsumptionStates(basePath, type, delta, deltaVolume);
532
+
533
+ // 5. HT/NT Tracking
534
+ await this._updateHTNTConsumptionStates(basePath, type, delta, config);
535
+
536
+ // 6. Yearly & Costs
537
+ const yearlyAmountFinal = await this._updateYearlyConsumption(
538
+ type,
539
+ meterName,
540
+ config,
541
+ consumption,
542
+ consumptionM3,
543
+ delta,
544
+ basePath,
545
+ );
546
+
547
+ await this.updateCosts(type, meterName, config, yearlyAmountFinal);
548
+ await this.updateTotalCosts(type);
549
+
550
+ await this.adapter.setStateAsync(`${basePath}.consumption.lastUpdate`, now, true);
551
+ await this.adapter.setStateAsync(`${basePath}.info.lastSync`, now, true);
552
+ }
553
+
554
+ /**
555
+ * Preprocesses raw sensor value (offset, gas conversion)
556
+ *
557
+ * @param {string} type - Utility type
558
+ * @param {number} value - Raw sensor value
559
+ * @param {object} config - Meter configuration
560
+ * @returns {Promise<{consumption: number, consumptionM3: number|null}>} Processed values
561
+ */
562
+ async _preprocessValue(type, value, config) {
371
563
  let consumption = value;
372
564
  let consumptionM3 = null;
373
565
 
374
- this.adapter.log.debug(`[${basePath}] Sensor update: raw=${value}, offset=${config.offset}`);
375
-
376
- // Apply offset FIRST
377
566
  if (config.offset !== 0) {
378
567
  consumption = consumption - config.offset;
379
- this.adapter.log.debug(`[${basePath}] After offset: ${consumption}`);
380
568
  }
381
569
 
382
- // For gas, convert m³ to kWh
383
570
  if (type === 'gas') {
384
571
  const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
385
572
  const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
386
- consumptionM3 = consumption;
387
- await this.adapter.setStateAsync(`${basePath}.info.meterReadingVolume`, consumption, true);
388
- consumption = calculator.convertGasM3ToKWh(consumption, brennwert, zZahl);
389
- consumption = calculator.roundToDecimals(consumption, 2);
390
- }
391
573
 
392
- // Update meter reading
393
- await this.adapter.setStateAsync(`${basePath}.info.meterReading`, consumption, true);
574
+ const res = consumptionHelper.calculateGas(consumption, brennwert, zZahl);
575
+ consumptionM3 = res.volume;
576
+ consumption = res.energy;
577
+ }
394
578
 
395
- // Calculate deltas
396
- const lastValue = this.lastSensorValues[sensorDP];
397
- this.lastSensorValues[sensorDP] = consumption;
579
+ return { consumption, consumptionM3 };
580
+ }
398
581
 
399
- if (lastValue === undefined || consumption <= lastValue) {
400
- if (lastValue !== undefined && consumption < lastValue) {
582
+ /**
583
+ * Handles the very first sensor value in a session (recovery or initial)
584
+ *
585
+ * @param {string} type - Utility type
586
+ * @param {string} meterName - Meter name
587
+ * @param {string} sensorDP - Sensor data point path
588
+ * @param {object} processed - Processed consumption values
589
+ * @param {string} basePath - State base path
590
+ * @param {object} config - Meter configuration
591
+ * @param {number} now - Current timestamp
592
+ */
593
+ async _handleFirstSensorValue(type, meterName, sensorDP, processed, basePath, config, now) {
594
+ const { consumption, consumptionM3 } = processed;
595
+ const currentState = await this.adapter.getStateAsync(`${basePath}.info.meterReading`);
596
+ const recoveredValue = currentState?.val ?? 0;
597
+
598
+ if (recoveredValue > 0 && Math.abs(consumption - recoveredValue) < 100) {
599
+ this.adapter.log.info(`[${basePath}] Recovered persistent baseline: ${recoveredValue}`);
600
+ this.lastSensorValues[sensorDP] = recoveredValue;
601
+
602
+ // Validate period consumption values against spike threshold
603
+ // This catches cases where old consumption values are unrealistically high
604
+ await this._validatePeriodConsumption(type, basePath, now);
605
+ } else {
606
+ if (recoveredValue > 0) {
401
607
  this.adapter.log.warn(
402
- `${type}.${meterName}: Sensor value decreased (${lastValue} -> ${consumption}). Assuming meter reset.`,
608
+ `[${basePath}] Recovered state (${recoveredValue}) differs significantly from new value (${consumption}).`,
609
+ );
610
+ } else {
611
+ this.adapter.log.info(
612
+ `[${basePath}] No previous reading found. Setting initial baseline to ${consumption}`,
403
613
  );
404
614
  }
405
- await this.updateCosts(type, meterName, config);
406
- await this.updateTotalCosts(type);
407
- return;
615
+ this.lastSensorValues[sensorDP] = consumption;
616
+ await this.adapter.setStateAsync(`${basePath}.info.meterReading`, consumption, true);
617
+ if (type === 'gas') {
618
+ await this.adapter.setStateAsync(`${basePath}.info.meterReadingVolume`, consumptionM3 || 0, true);
619
+ }
620
+
621
+ // On baseline reset, validate and potentially reset period consumption values
622
+ await this._validatePeriodConsumption(type, basePath, now);
623
+
624
+ if (config.initialReading > 0) {
625
+ await this.calculateAbsoluteYearly(type, meterName, config, consumption, consumptionM3 || 0, now);
626
+ await this.updateCosts(type, meterName, config);
627
+ await this.updateTotalCosts(type);
628
+ }
408
629
  }
630
+ await this.adapter.setStateAsync(`${basePath}.info.lastSync`, now, true);
631
+ await this.adapter.setStateAsync(`${basePath}.consumption.lastUpdate`, now, true);
632
+ }
409
633
 
410
- const delta = consumption - lastValue;
411
- this.adapter.log.debug(`${type}.${meterName} delta: ${delta}`);
634
+ /**
635
+ * Validates period consumption values and resets them if they exceed the spike threshold.
636
+ * This prevents unrealistic values after adapter restart or database inconsistencies.
637
+ *
638
+ * @param {string} type - Utility type
639
+ * @param {string} basePath - State base path
640
+ * @param {number} now - Current timestamp
641
+ */
642
+ async _validatePeriodConsumption(type, basePath, now) {
643
+ const spikeThreshold = this.adapter.config.sensorSpikeThreshold || DEFAULT_SPIKE_THRESHOLD;
644
+
645
+ // Check and reset period consumption values that exceed the spike threshold
646
+ const periods = ['daily', 'weekly', 'monthly'];
647
+ for (const period of periods) {
648
+ const state = await this.adapter.getStateAsync(`${basePath}.consumption.${period}`);
649
+ const value = state?.val || 0;
650
+
651
+ // Get period start timestamp to calculate expected max consumption
652
+ const periodStartState = await this.adapter.getStateAsync(
653
+ `${basePath}.statistics.last${period.charAt(0).toUpperCase() + period.slice(1)}Start`,
654
+ );
655
+ const periodStart = periodStartState?.val || now;
656
+ const daysSincePeriodStart = (now - periodStart) / (24 * 60 * 60 * 1000);
412
657
 
413
- // Track volume for gas
414
- if (type === 'gas') {
415
- const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
416
- const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
417
- const deltaVolume = delta / (brennwert * zZahl);
658
+ // Calculate reasonable max: spike threshold per day * days in period
659
+ // Add buffer of 2x for safety
660
+ const maxReasonableConsumption = spikeThreshold * Math.max(1, daysSincePeriodStart) * 2;
661
+
662
+ if (value > maxReasonableConsumption) {
663
+ this.adapter.log.warn(
664
+ `[${basePath}] Resetting ${period} consumption: ${value} exceeds reasonable max of ${maxReasonableConsumption.toFixed(0)} (${daysSincePeriodStart.toFixed(1)} days * ${spikeThreshold} threshold * 2)`,
665
+ );
666
+ await this.adapter.setStateAsync(`${basePath}.consumption.${period}`, 0, true);
418
667
 
419
- const dailyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.dailyVolume`);
420
- const monthlyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.monthlyVolume`);
421
- const yearlyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.yearlyVolume`);
668
+ // Also reset volume states for gas
669
+ if (type === 'gas') {
670
+ await this.adapter.setStateAsync(`${basePath}.consumption.${period}Volume`, 0, true);
671
+ }
672
+ }
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Handles meter reset or replacement condition
678
+ *
679
+ * @param {string} type - Utility type
680
+ * @param {string} meterName - Meter name
681
+ * @param {number} lastValue - Previous sensor value
682
+ * @param {number} consumption - Current consumption value
683
+ * @param {object} config - Meter configuration
684
+ */
685
+ async _handleMeterReset(type, meterName, lastValue, consumption, config) {
686
+ this.adapter.log.warn(
687
+ `${type}.${meterName}: Zählerstand gesunken (${lastValue} -> ${consumption}). Gehe von Zählerwechsel oder Reset aus.`,
688
+ );
689
+ await this.updateCosts(type, meterName, config);
690
+ await this.updateTotalCosts(type);
691
+ }
422
692
 
693
+ /**
694
+ * Handles suspicious delta (spike detection)
695
+ *
696
+ * @param {string} type - Utility type
697
+ * @param {string} meterName - Meter name
698
+ * @param {number} delta - Calculated delta
699
+ * @param {number} consumption - Current consumption value
700
+ * @param {number|null} consumptionM3 - Current gas volume
701
+ * @param {object} config - Meter configuration
702
+ * @param {string} basePath - State base path
703
+ * @param {number} now - Current timestamp
704
+ */
705
+ async _handleSuspiciousDelta(type, meterName, delta, consumption, consumptionM3, config, basePath, now) {
706
+ this.adapter.log.warn(`[${basePath}] Discarding suspicious delta of ${delta}. Treating as baseline reset.`);
707
+ if (config.initialReading > 0) {
708
+ await this.calculateAbsoluteYearly(type, meterName, config, consumption, consumptionM3 || 0, now);
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Updates all period consumption states
714
+ *
715
+ * @param {string} basePath - State base path
716
+ * @param {string} type - Utility type
717
+ * @param {number} delta - Consumption delta
718
+ * @param {number} deltaVolume - Gas volume delta
719
+ */
720
+ async _updateTotalConsumptionStates(basePath, type, delta, deltaVolume) {
721
+ const periods = ['daily', 'weekly', 'monthly'];
722
+ for (const period of periods) {
723
+ const state = await this.adapter.getStateAsync(`${basePath}.consumption.${period}`);
423
724
  await this.adapter.setStateAsync(
424
- `${basePath}.consumption.dailyVolume`,
425
- calculator.roundToDecimals((dailyVolume?.val || 0) + deltaVolume, 2),
725
+ `${basePath}.consumption.${period}`,
726
+ calculator.roundToDecimals((state?.val || 0) + delta, 2),
426
727
  true,
427
728
  );
729
+
730
+ if (type === 'gas' && deltaVolume > 0) {
731
+ const volState = await this.adapter.getStateAsync(`${basePath}.consumption.${period}Volume`);
732
+ await this.adapter.setStateAsync(
733
+ `${basePath}.consumption.${period}Volume`,
734
+ calculator.roundToDecimals((volState?.val || 0) + deltaVolume, 2),
735
+ true,
736
+ );
737
+ }
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Updates HT/NT specific consumption states
743
+ *
744
+ * @param {string} basePath - State base path
745
+ * @param {string} type - Utility type
746
+ * @param {number} delta - Consumption delta
747
+ * @param {object} config - Meter configuration
748
+ */
749
+ async _updateHTNTConsumptionStates(basePath, type, delta, config) {
750
+ if (!config.htNtEnabled) {
751
+ return;
752
+ }
753
+
754
+ const configType = this.getConfigType(type);
755
+ const suffix = consumptionHelper.getHTNTSuffix(this.adapter.config, configType);
756
+ if (!suffix) {
757
+ return;
758
+ }
759
+
760
+ const periods = ['daily', 'weekly', 'monthly'];
761
+ for (const period of periods) {
762
+ const state = await this.adapter.getStateAsync(`${basePath}.consumption.${period}${suffix}`);
428
763
  await this.adapter.setStateAsync(
429
- `${basePath}.consumption.monthlyVolume`,
430
- calculator.roundToDecimals((monthlyVolume?.val || 0) + deltaVolume, 2),
764
+ `${basePath}.consumption.${period}${suffix}`,
765
+ calculator.roundToDecimals((state?.val || 0) + delta, 2),
431
766
  true,
432
767
  );
768
+ }
769
+
770
+ if (type === 'gas') {
771
+ const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
772
+ const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
773
+ const deltaVolume = delta / (brennwert * zZahl);
774
+
775
+ const state = await this.adapter.getStateAsync(`${basePath}.consumption.weeklyVolume${suffix}`);
433
776
  await this.adapter.setStateAsync(
434
- `${basePath}.consumption.yearlyVolume`,
435
- calculator.roundToDecimals((yearlyVolume?.val || 0) + deltaVolume, 3),
777
+ `${basePath}.consumption.weeklyVolume${suffix}`,
778
+ calculator.roundToDecimals((state?.val || 0) + deltaVolume, 2),
436
779
  true,
437
780
  );
438
781
  }
782
+ }
439
783
 
440
- // Update consumption values
441
- const dailyState = await this.adapter.getStateAsync(`${basePath}.consumption.daily`);
442
- await this.adapter.setStateAsync(
443
- `${basePath}.consumption.daily`,
444
- calculator.roundToDecimals((dailyState?.val || 0) + delta, 2),
445
- true,
446
- );
447
-
448
- const monthlyState = await this.adapter.getStateAsync(`${basePath}.consumption.monthly`);
449
- await this.adapter.setStateAsync(
450
- `${basePath}.consumption.monthly`,
451
- calculator.roundToDecimals((monthlyState?.val || 0) + delta, 2),
452
- true,
453
- );
454
-
455
- // Yearly consumption
784
+ /**
785
+ * Updates yearly consumption
786
+ *
787
+ * @param {string} type - Utility type
788
+ * @param {string} meterName - Meter name
789
+ * @param {object} config - Meter configuration
790
+ * @param {number} consumption - Current consumption value
791
+ * @param {number|null} consumptionM3 - Current gas volume
792
+ * @param {number} delta - Consumption delta
793
+ * @param {string} basePath - State base path
794
+ * @returns {Promise<number>} final yearly amount
795
+ */
796
+ async _updateYearlyConsumption(type, meterName, config, consumption, consumptionM3, delta, basePath) {
797
+ let yearlyAmountFinal;
456
798
  if (config.initialReading > 0) {
457
799
  let yearlyAmount;
458
800
  if (type === 'gas') {
@@ -468,21 +810,55 @@ class MultiMeterManager {
468
810
  } else {
469
811
  yearlyAmount = Math.max(0, consumption - config.initialReading);
470
812
  }
471
- await this.adapter.setStateAsync(
472
- `${basePath}.consumption.yearly`,
473
- calculator.roundToDecimals(yearlyAmount, 2),
474
- true,
475
- );
813
+ yearlyAmountFinal = calculator.roundToDecimals(yearlyAmount, 2);
814
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyAmountFinal, true);
476
815
  } else {
477
816
  const yState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
817
+ yearlyAmountFinal = calculator.roundToDecimals((yState?.val || 0) + delta, 2);
818
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyAmountFinal, true);
819
+ }
820
+ return yearlyAmountFinal;
821
+ }
822
+
823
+ /**
824
+ * Helper to calculate absolute yearly consumption (usually on start/reset)
825
+ *
826
+ * @param {string} type - Utility type
827
+ * @param {string} meterName - Meter name
828
+ * @param {object} config - Meter configuration
829
+ * @param {number} consumption - Current consumption value
830
+ * @param {number} consumptionM3 - Current consumption in m³ (for gas)
831
+ * @param {number} now - Current timestamp
832
+ */
833
+ async calculateAbsoluteYearly(type, meterName, config, consumption, consumptionM3, now) {
834
+ const basePath = `${type}.${meterName}`;
835
+ let yearlyAmountFinal;
836
+
837
+ let yearlyAmount;
838
+ if (type === 'gas') {
839
+ const yearlyM3 = Math.max(0, (consumptionM3 || 0) - config.initialReading);
840
+ this.adapter.log.debug(
841
+ `[${basePath}] Yearly absolute (Gas): consumptionM3=${consumptionM3}, initialReading=${config.initialReading}, resultM3=${yearlyM3}`,
842
+ );
478
843
  await this.adapter.setStateAsync(
479
- `${basePath}.consumption.yearly`,
480
- calculator.roundToDecimals((yState?.val || 0) + delta, 2),
844
+ `${basePath}.consumption.yearlyVolume`,
845
+ calculator.roundToDecimals(yearlyM3, 2),
481
846
  true,
482
847
  );
848
+ const brennwert = this.adapter.config.gasBrennwert || calculator.DEFAULTS.GAS_BRENNWERT;
849
+ const zZahl = this.adapter.config.gasZahl || calculator.DEFAULTS.GAS_Z_ZAHL;
850
+ yearlyAmount = calculator.convertGasM3ToKWh(yearlyM3, brennwert, zZahl);
851
+ } else {
852
+ yearlyAmount = Math.max(0, consumption - config.initialReading);
853
+ this.adapter.log.debug(
854
+ `[${basePath}] Yearly absolute: consumption=${consumption}, initialReading=${config.initialReading}, result=${yearlyAmount}`,
855
+ );
483
856
  }
857
+ yearlyAmountFinal = calculator.roundToDecimals(yearlyAmount, 2);
858
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, yearlyAmountFinal, true);
484
859
 
485
- await this.updateCosts(type, meterName, config);
860
+ // Pass direct calculated values to updateCosts to avoid race condition with DB
861
+ await this.updateCosts(type, meterName, config, yearlyAmountFinal);
486
862
  await this.updateTotalCosts(type);
487
863
 
488
864
  await this.adapter.setStateAsync(`${basePath}.consumption.lastUpdate`, now, true);
@@ -522,6 +898,7 @@ class MultiMeterManager {
522
898
  true,
523
899
  );
524
900
  await this.adapter.setStateAsync(`${basePath}.info.currentTariff`, tariffName, true);
901
+ await this.adapter.setStateAsync(`${basePath}.info.lastSync`, Date.now(), true);
525
902
  }
526
903
 
527
904
  /**
@@ -530,14 +907,21 @@ class MultiMeterManager {
530
907
  * @param {string} type - Utility type
531
908
  * @param {string} meterName - Meter name
532
909
  * @param {object} config - Meter configuration
910
+ * @param {number} [forcedYearly] - Optional already calculated yearly consumption (avoids DB race)
533
911
  */
534
- async updateCosts(type, meterName, config) {
912
+ async updateCosts(type, meterName, config, forcedYearly) {
535
913
  const basePath = `${type}.${meterName}`;
536
914
 
537
915
  // Get consumption values
538
916
  const daily = (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
539
917
  const monthly = (await this.adapter.getStateAsync(`${basePath}.consumption.monthly`))?.val || 0;
540
- const yearly = (await this.adapter.getStateAsync(`${basePath}.consumption.yearly`))?.val || 0;
918
+
919
+ let yearly;
920
+ if (forcedYearly !== undefined && forcedYearly !== null) {
921
+ yearly = forcedYearly;
922
+ } else {
923
+ yearly = (await this.adapter.getStateAsync(`${basePath}.consumption.yearly`))?.val || 0;
924
+ }
541
925
 
542
926
  // Get price
543
927
  const price = config.preis || 0;
@@ -548,67 +932,66 @@ class MultiMeterManager {
548
932
 
549
933
  // Calculate consumption costs
550
934
  const dailyCost = daily * price;
935
+ const weeklyState = await this.adapter.getStateAsync(`${basePath}.consumption.weekly`);
936
+ const weeklyCost = (weeklyState?.val || 0) * price;
551
937
  const monthlyCost = monthly * price;
552
938
  const yearlyCost = yearly * price;
553
939
 
554
940
  await this.adapter.setStateAsync(`${basePath}.costs.daily`, calculator.roundToDecimals(dailyCost, 2), true);
941
+ await this.adapter.setStateAsync(`${basePath}.costs.weekly`, calculator.roundToDecimals(weeklyCost, 2), true);
555
942
  await this.adapter.setStateAsync(`${basePath}.costs.monthly`, calculator.roundToDecimals(monthlyCost, 2), true);
556
943
  await this.adapter.setStateAsync(`${basePath}.costs.yearly`, calculator.roundToDecimals(yearlyCost, 2), true);
557
944
 
558
945
  // Calculate accumulated costs based on contract start
559
- const yearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
560
- let monthsSinceYearStart = 1;
561
- let basicChargeAccumulated = 0;
946
+ const monthsSinceYearStart = await this._calculateMonthsSinceYearStart(basePath);
562
947
 
563
- if (yearStartState && yearStartState.val) {
564
- // yearStartState.val is a timestamp (number)
565
- const yearStartDate = new Date(yearStartState.val);
566
- if (!isNaN(yearStartDate.getTime())) {
567
- const now = new Date();
568
-
569
- // Calculate months since contract start (started months, including current)
570
- monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, now) + 1;
571
-
572
- // Calculate accumulated basic charge (monthly fee × months)
573
- basicChargeAccumulated = (config.grundgebuehr || 0) * monthsSinceYearStart;
574
- }
575
- }
576
-
577
- // Jahresgebühr ist ein FIXER Wert pro Jahr (z.B. 60€)
578
- // NICHT pro-rata nach Monaten/Tagen berechnen!
579
- const annualFeeAccumulated = config.jahresgebuehr || 0;
580
-
581
- // Update basicCharge with accumulated value (not just monthly!)
582
- await this.adapter.setStateAsync(
583
- `${basePath}.costs.basicCharge`,
584
- calculator.roundToDecimals(basicChargeAccumulated, 2),
585
- true,
948
+ const charges = billingHelper.calculateAccumulatedCharges(
949
+ config.grundgebuehr,
950
+ config.jahresgebuehr,
951
+ monthsSinceYearStart,
586
952
  );
953
+ const basicChargeAccumulated = charges.basicCharge;
954
+ const annualFeeAccumulated = charges.annualFee;
587
955
 
588
- await this.adapter.setStateAsync(
589
- `${basePath}.costs.annualFee`,
590
- calculator.roundToDecimals(annualFeeAccumulated, 2),
591
- true,
592
- );
956
+ // Update basicCharge and annualFee states
957
+ await this.adapter.setStateAsync(`${basePath}.costs.basicCharge`, basicChargeAccumulated, true);
958
+ await this.adapter.setStateAsync(`${basePath}.costs.annualFee`, annualFeeAccumulated, true);
593
959
 
594
960
  // Calculate total yearly costs and balance
595
- const totalYearlyCost = yearlyCost + basicChargeAccumulated + annualFeeAccumulated;
596
-
961
+ const totalYearlyCost = Math.max(0, yearlyCost + charges.total);
597
962
  await this.adapter.setStateAsync(
598
963
  `${basePath}.costs.totalYearly`,
599
964
  calculator.roundToDecimals(totalYearlyCost, 2),
600
965
  true,
601
966
  );
602
967
 
603
- const paidTotal = (config.abschlag || 0) * monthsSinceYearStart;
604
- const balance = totalYearlyCost - paidTotal;
968
+ const balanceRes = billingHelper.calculateBalance(config.abschlag, monthsSinceYearStart, totalYearlyCost);
605
969
 
606
970
  this.adapter.log.debug(
607
- `[${basePath}] Balance calculation: grundgebuehr=${config.grundgebuehr}, jahresgebuehr=${config.jahresgebuehr}, abschlag=${config.abschlag}, months=${monthsSinceYearStart}, basicCharge=${basicChargeAccumulated.toFixed(2)}, annualFee=${annualFeeAccumulated.toFixed(2)}, paidTotal=${paidTotal.toFixed(2)}, totalYearly=${totalYearlyCost.toFixed(2)}, balance=${balance.toFixed(2)}`,
971
+ `[${basePath}] Cost calculation (MultiMeter): daily=${daily}, monthly=${monthly}, yearly=${yearly}, price=${price}, totalYearly=${totalYearlyCost}, paidTotal=${balanceRes.paid}, balance=${balanceRes.balance}`,
608
972
  );
609
973
 
610
- await this.adapter.setStateAsync(`${basePath}.costs.paidTotal`, calculator.roundToDecimals(paidTotal, 2), true);
611
- await this.adapter.setStateAsync(`${basePath}.costs.balance`, calculator.roundToDecimals(balance, 2), true);
974
+ await this.adapter.setStateAsync(`${basePath}.costs.paidTotal`, balanceRes.paid, true);
975
+ await this.adapter.setStateAsync(`${basePath}.costs.balance`, balanceRes.balance, true);
976
+ }
977
+
978
+ /**
979
+ * Calculates months since year start for a meter
980
+ *
981
+ * @param {string} basePath - State base path
982
+ * @returns {Promise<number>} Months since start (at least 1)
983
+ */
984
+ async _calculateMonthsSinceYearStart(basePath) {
985
+ const yearStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastYearStart`);
986
+ let monthsSinceYearStart = 1;
987
+
988
+ if (yearStartState && yearStartState.val) {
989
+ const yearStartDate = new Date(yearStartState.val);
990
+ if (!isNaN(yearStartDate.getTime())) {
991
+ monthsSinceYearStart = calculator.getMonthsDifference(yearStartDate, new Date()) + 1;
992
+ }
993
+ }
994
+ return Math.max(1, monthsSinceYearStart);
612
995
  }
613
996
 
614
997
  /**
@@ -624,10 +1007,22 @@ class MultiMeterManager {
624
1007
  return;
625
1008
  }
626
1009
 
1010
+ // Check if totals structure exists before trying to update
1011
+ const totalsExists = await this.adapter.getObjectAsync(`${type}.totals`);
1012
+ if (!totalsExists) {
1013
+ // Totals structure not yet created, skip update
1014
+ // This happens during initialization when handleSensorUpdate is called
1015
+ // before createTotalsStructure has run
1016
+ this.adapter.log.debug(`Skipping total costs update for ${type} - totals structure not yet created`);
1017
+ return;
1018
+ }
1019
+
627
1020
  let totalDaily = 0;
1021
+ let totalWeekly = 0;
628
1022
  let totalMonthly = 0;
629
1023
  let totalYearly = 0;
630
1024
  let totalCostsDaily = 0;
1025
+ let totalCostsWeekly = 0;
631
1026
  let totalCostsMonthly = 0;
632
1027
  let totalCostsYearly = 0;
633
1028
 
@@ -635,10 +1030,12 @@ class MultiMeterManager {
635
1030
  const basePath = `${type}.${meter.name}`;
636
1031
 
637
1032
  totalDaily += (await this.adapter.getStateAsync(`${basePath}.consumption.daily`))?.val || 0;
1033
+ totalWeekly += (await this.adapter.getStateAsync(`${basePath}.consumption.weekly`))?.val || 0;
638
1034
  totalMonthly += (await this.adapter.getStateAsync(`${basePath}.consumption.monthly`))?.val || 0;
639
1035
  totalYearly += (await this.adapter.getStateAsync(`${basePath}.consumption.yearly`))?.val || 0;
640
1036
 
641
1037
  totalCostsDaily += (await this.adapter.getStateAsync(`${basePath}.costs.daily`))?.val || 0;
1038
+ totalCostsWeekly += (await this.adapter.getStateAsync(`${basePath}.costs.weekly`))?.val || 0;
642
1039
  totalCostsMonthly += (await this.adapter.getStateAsync(`${basePath}.costs.monthly`))?.val || 0;
643
1040
  totalCostsYearly += (await this.adapter.getStateAsync(`${basePath}.costs.totalYearly`))?.val || 0;
644
1041
  }
@@ -648,6 +1045,11 @@ class MultiMeterManager {
648
1045
  calculator.roundToDecimals(totalDaily, 2),
649
1046
  true,
650
1047
  );
1048
+ await this.adapter.setStateAsync(
1049
+ `${type}.totals.consumption.weekly`,
1050
+ calculator.roundToDecimals(totalWeekly, 2),
1051
+ true,
1052
+ );
651
1053
  await this.adapter.setStateAsync(
652
1054
  `${type}.totals.consumption.monthly`,
653
1055
  calculator.roundToDecimals(totalMonthly, 2),
@@ -664,6 +1066,11 @@ class MultiMeterManager {
664
1066
  calculator.roundToDecimals(totalCostsDaily, 2),
665
1067
  true,
666
1068
  );
1069
+ await this.adapter.setStateAsync(
1070
+ `${type}.totals.costs.weekly`,
1071
+ calculator.roundToDecimals(totalCostsWeekly, 2),
1072
+ true,
1073
+ );
667
1074
  await this.adapter.setStateAsync(
668
1075
  `${type}.totals.costs.monthly`,
669
1076
  calculator.roundToDecimals(totalCostsMonthly, 2),