iobroker.utility-monitor 1.5.0 → 1.6.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.
@@ -171,7 +171,7 @@ class BillingManager {
171
171
  }
172
172
 
173
173
  if (!startDate || isNaN(startDate.getTime())) {
174
- const lastYearStart = await this.adapter.getStateAsync(`${type}.statistics.lastYearStart`);
174
+ const lastYearStart = await this.adapter.getStateAsync(`${type}.statistics.timestamps.lastYearStart`);
175
175
  startDate = new Date(lastYearStart?.val || Date.now());
176
176
  }
177
177
 
@@ -372,7 +372,11 @@ class BillingManager {
372
372
  // even if the user closes the period early (e.g. 2 days before)
373
373
  const thisYearAnniversary = new Date(startDate);
374
374
  thisYearAnniversary.setFullYear(new Date().getFullYear());
375
- await this.adapter.setStateAsync(`${type}.statistics.lastYearStart`, thisYearAnniversary.getTime(), true);
375
+ await this.adapter.setStateAsync(
376
+ `${type}.statistics.timestamps.lastYearStart`,
377
+ thisYearAnniversary.getTime(),
378
+ true,
379
+ );
376
380
 
377
381
  this.adapter.log.info(`✅ Abrechnungszeitraum ${year} für ${type} erfolgreich abgeschlossen!`);
378
382
  this.adapter.log.info(
@@ -419,11 +423,96 @@ class BillingManager {
419
423
  return;
420
424
  }
421
425
 
426
+ // Archive data for this meter
422
427
  const year = startDate.getFullYear();
423
428
 
424
- // Archive data for this meter
425
- // TODO: Implement full history archiving for individual meters
426
- // For now, just reset the meter
429
+ this.adapter.log.info(`Archiving data for ${basePath} year ${year}...`);
430
+
431
+ // Create history structure for this meter
432
+ await this.adapter.setObjectNotExistsAsync(`${basePath}.history`, {
433
+ type: 'channel',
434
+ common: { name: 'Historie' },
435
+ native: {},
436
+ });
437
+ await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}`, {
438
+ type: 'channel',
439
+ common: { name: `Jahr ${year}` },
440
+ native: {},
441
+ });
442
+
443
+ // Get current values for archiving
444
+ const yearlyState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
445
+ const totalYearlyState = await this.adapter.getStateAsync(`${basePath}.costs.totalYearly`);
446
+ const balanceState = await this.adapter.getStateAsync(`${basePath}.costs.balance`);
447
+
448
+ const yearly = yearlyState?.val || 0;
449
+ const totalYearly = totalYearlyState?.val || 0;
450
+ const balance = balanceState?.val || 0;
451
+
452
+ const consumptionUnit = type === 'gas' ? 'kWh' : type === 'water' ? 'm³' : 'kWh';
453
+
454
+ // Archive yearly consumption
455
+ await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.yearly`, {
456
+ type: 'state',
457
+ common: {
458
+ name: `Jahresverbrauch ${year}`,
459
+ type: 'number',
460
+ role: 'value',
461
+ read: true,
462
+ write: false,
463
+ unit: consumptionUnit,
464
+ },
465
+ native: {},
466
+ });
467
+ await this.adapter.setStateAsync(`${basePath}.history.${year}.yearly`, yearly, true);
468
+
469
+ // Archive gas volume if applicable
470
+ if (type === 'gas') {
471
+ const yearlyVolume = (await this.adapter.getStateAsync(`${basePath}.consumption.yearlyVolume`))?.val || 0;
472
+ await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.yearlyVolume`, {
473
+ type: 'state',
474
+ common: {
475
+ name: `Jahresverbrauch ${year} (m³)`,
476
+ type: 'number',
477
+ role: 'value',
478
+ read: true,
479
+ write: false,
480
+ unit: 'm³',
481
+ },
482
+ native: {},
483
+ });
484
+ await this.adapter.setStateAsync(`${basePath}.history.${year}.yearlyVolume`, yearlyVolume, true);
485
+ }
486
+
487
+ // Archive total yearly costs
488
+ await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.totalYearly`, {
489
+ type: 'state',
490
+ common: {
491
+ name: `Gesamtkosten ${year}`,
492
+ type: 'number',
493
+ role: 'value.money',
494
+ read: true,
495
+ write: false,
496
+ unit: '€',
497
+ },
498
+ native: {},
499
+ });
500
+ await this.adapter.setStateAsync(`${basePath}.history.${year}.totalYearly`, totalYearly, true);
501
+
502
+ // Archive balance
503
+ await this.adapter.setObjectNotExistsAsync(`${basePath}.history.${year}.balance`, {
504
+ type: 'state',
505
+ common: {
506
+ name: `Bilanz ${year}`,
507
+ type: 'number',
508
+ role: 'value.money',
509
+ read: true,
510
+ write: false,
511
+ unit: '€',
512
+ },
513
+ native: {},
514
+ });
515
+ await this.adapter.setStateAsync(`${basePath}.history.${year}.balance`, balance, true);
427
516
 
428
517
  // Reset consumption and costs for this meter
429
518
  await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, 0, true);
@@ -441,7 +530,11 @@ class BillingManager {
441
530
  // Update lastYearStart to contract anniversary
442
531
  const thisYearAnniversary = new Date(startDate);
443
532
  thisYearAnniversary.setFullYear(new Date().getFullYear());
444
- await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, thisYearAnniversary.getTime(), true);
533
+ await this.adapter.setStateAsync(
534
+ `${basePath}.statistics.timestamps.lastYearStart`,
535
+ thisYearAnniversary.getTime(),
536
+ true,
537
+ );
445
538
 
446
539
  // Update totals if multiple meters exist
447
540
  const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
@@ -539,64 +632,100 @@ class BillingManager {
539
632
  // This ensures History adapter sees clean day boundaries
540
633
  const isResetTime = nowDate.getHours() === 23 && nowDate.getMinutes() === 59;
541
634
 
542
- // Helper: Check if timestamp is from today
635
+ // Helper: Create normalized timestamp for 23:59:00 of a given date
636
+ // This ensures consistent timestamps regardless of actual execution time
637
+ const getNormalizedResetTime = date => {
638
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 0, 0).getTime();
639
+ };
640
+
641
+ // Helper: Check if timestamp is from a valid 23:59 reset today
642
+ // A valid reset must be from today AND from the 23:xx hour
643
+ // This prevents adapter restarts (e.g. at 00:01:03) from blocking the real 23:59 reset
543
644
  const todayStart = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate()).getTime();
544
- const isFromToday = timestamp => timestamp >= todayStart;
645
+ const isValidResetToday = timestamp => {
646
+ if (timestamp < todayStart) {
647
+ return false; // Not from today
648
+ }
649
+ // Check if the timestamp was from the 23:xx hour (valid reset time)
650
+ const resetDate = new Date(timestamp);
651
+ return resetDate.getHours() === 23;
652
+ };
545
653
 
546
654
  // DAILY RESET: Trigger at 23:59 if today's reset hasn't happened yet
547
- const lastDayStart = await this.adapter.getStateAsync(`${basePath}.statistics.lastDayStart`);
655
+ const lastDayStart = await this.adapter.getStateAsync(`${basePath}.statistics.timestamps.lastDayStart`);
548
656
  if (lastDayStart?.val) {
549
657
  const lastResetTime = lastDayStart.val;
550
- const alreadyResetToday = isFromToday(lastResetTime);
658
+ const alreadyResetToday = isValidResetToday(lastResetTime);
551
659
 
552
660
  // Reset at 23:59 if not yet reset today, OR catch up if we missed it (e.g. adapter was offline)
553
661
  if (isResetTime && !alreadyResetToday) {
554
662
  this.adapter.log.info(`Täglicher Reset für ${type} um 23:59`);
555
- await this.resetDailyCounters(type);
663
+ await this.resetDailyCounters(type, getNormalizedResetTime(nowDate));
556
664
  } else if (!alreadyResetToday && nowDate.getTime() > lastResetTime + 24 * 60 * 60 * 1000) {
557
665
  // Catch-up: More than 24h since last reset (adapter was offline)
666
+ // Calculate when the reset SHOULD have happened (yesterday 23:59)
667
+ const yesterday = new Date(nowDate);
668
+ yesterday.setDate(yesterday.getDate() - 1);
669
+ const catchUpTime = getNormalizedResetTime(yesterday);
558
670
  this.adapter.log.info(`Täglicher Reset für ${type} (Nachholung - Adapter war offline)`);
559
- await this.resetDailyCounters(type);
671
+ await this.resetDailyCounters(type, catchUpTime);
560
672
  }
561
673
  }
562
674
 
563
675
  // WEEKLY RESET: Trigger at 23:59 on Sunday if this week's reset hasn't happened yet
564
- const lastWeekStart = await this.adapter.getStateAsync(`${basePath}.statistics.lastWeekStart`);
676
+ const lastWeekStart = await this.adapter.getStateAsync(`${basePath}.statistics.timestamps.lastWeekStart`);
565
677
  if (lastWeekStart?.val) {
566
678
  const lastWeekTime = lastWeekStart.val;
567
679
  const isSunday = nowDate.getDay() === 0; // 0 = Sunday
568
680
 
569
681
  // Check if we're in a new week (more than 6 days since last reset)
682
+ // BUT also check if the last reset was actually a valid 23:59 reset
683
+ // to prevent adapter restarts from blocking the real weekly reset
570
684
  const daysSinceLastReset = (nowDate.getTime() - lastWeekTime) / (24 * 60 * 60 * 1000);
571
- const needsWeeklyReset = daysSinceLastReset >= 6;
685
+ const lastWeekResetDate = new Date(lastWeekTime);
686
+ const wasValidWeeklyReset = lastWeekResetDate.getHours() === 23;
687
+ const needsWeeklyReset = daysSinceLastReset >= 6 || (isSunday && !wasValidWeeklyReset);
572
688
 
573
689
  if (isSunday && isResetTime && needsWeeklyReset) {
574
690
  this.adapter.log.info(`Wöchentlicher Reset für ${type} um 23:59`);
575
- await this.resetWeeklyCounters(type);
576
- } else if (needsWeeklyReset && daysSinceLastReset > 7) {
577
- // Catch-up: More than 7 days since last reset
691
+ await this.resetWeeklyCounters(type, getNormalizedResetTime(nowDate));
692
+ } else if (needsWeeklyReset && daysSinceLastReset >= 7) {
693
+ // Catch-up: 7 or more days since last reset (was offline or missed window)
694
+ // Calculate when the reset SHOULD have happened (last Sunday 23:59)
695
+ const lastSunday = new Date(nowDate);
696
+ lastSunday.setDate(lastSunday.getDate() - lastSunday.getDay()); // Go back to Sunday
697
+ const catchUpTime = getNormalizedResetTime(lastSunday);
578
698
  this.adapter.log.info(`Wöchentlicher Reset für ${type} (Nachholung)`);
579
- await this.resetWeeklyCounters(type);
699
+ await this.resetWeeklyCounters(type, catchUpTime);
580
700
  }
581
701
  }
582
702
 
583
703
  // MONTHLY RESET: Trigger at 23:59 on last day of month
584
704
  if (meters.length > 0) {
585
- const lastMonthStartState = await this.adapter.getStateAsync(`${basePath}.statistics.lastMonthStart`);
705
+ const lastMonthStartState = await this.adapter.getStateAsync(
706
+ `${basePath}.statistics.timestamps.lastMonthStart`,
707
+ );
586
708
  if (lastMonthStartState?.val) {
587
709
  const lastMonthTime = lastMonthStartState.val;
588
710
  const lastMonthDate = new Date(lastMonthTime);
589
711
  const isLastDayOfMonth =
590
712
  new Date(nowDate.getFullYear(), nowDate.getMonth() + 1, 0).getDate() === nowDate.getDate();
591
713
  const monthChanged = nowDate.getMonth() !== lastMonthDate.getMonth();
714
+ // Also check if the last reset was a valid 23:59 reset
715
+ // to prevent adapter restarts from blocking the real monthly reset
716
+ const wasValidMonthlyReset = lastMonthDate.getHours() === 23;
717
+ const needsMonthlyReset = monthChanged || (isLastDayOfMonth && !wasValidMonthlyReset);
592
718
 
593
- if (isLastDayOfMonth && isResetTime && monthChanged) {
719
+ if (isLastDayOfMonth && isResetTime && needsMonthlyReset) {
594
720
  this.adapter.log.info(`Monatlicher Reset für ${type} um 23:59`);
595
- await this.resetMonthlyCounters(type);
721
+ await this.resetMonthlyCounters(type, getNormalizedResetTime(nowDate));
596
722
  } else if (monthChanged && nowDate.getDate() > 1) {
597
723
  // Catch-up: We're past the 1st of a new month and haven't reset yet
724
+ // Calculate when the reset SHOULD have happened (last day of previous month 23:59)
725
+ const lastDayPrevMonth = new Date(nowDate.getFullYear(), nowDate.getMonth(), 0);
726
+ const catchUpTime = getNormalizedResetTime(lastDayPrevMonth);
598
727
  this.adapter.log.info(`Monatlicher Reset für ${type} (Nachholung)`);
599
- await this.resetMonthlyCounters(type);
728
+ await this.resetMonthlyCounters(type, catchUpTime);
600
729
  }
601
730
  }
602
731
  }
@@ -606,7 +735,7 @@ class BillingManager {
606
735
  for (const meter of meters) {
607
736
  const meterBasePath = `${type}.${meter.name}`;
608
737
  const lastYearStartState = await this.adapter.getStateAsync(
609
- `${meterBasePath}.statistics.lastYearStart`,
738
+ `${meterBasePath}.statistics.timestamps.lastYearStart`,
610
739
  );
611
740
 
612
741
  if (lastYearStartState?.val) {
@@ -638,17 +767,19 @@ class BillingManager {
638
767
  this.adapter.log.info(
639
768
  `Yearly reset for ${meterBasePath} um 23:59 (Vertragsjubiläum morgen: ${contractStartDate})`,
640
769
  );
641
- await this.resetYearlyCountersForMeter(type, meter);
770
+ await this.resetYearlyCountersForMeter(type, meter, getNormalizedResetTime(nowDate));
642
771
 
643
772
  if (meters.length > 1) {
644
773
  await this.adapter.multiMeterManager.updateTotalCosts(type);
645
774
  }
646
775
  } else if (isPastAnniversary && needsReset) {
647
776
  // Catch-up: Anniversary has passed but we haven't reset yet
777
+ // Use the day before anniversary 23:59 as the reset time
778
+ const catchUpTime = getNormalizedResetTime(dayBeforeAnniversary);
648
779
  this.adapter.log.info(
649
780
  `Yearly reset for ${meterBasePath} (Nachholung - Jubiläum: ${contractStartDate})`,
650
781
  );
651
- await this.resetYearlyCountersForMeter(type, meter);
782
+ await this.resetYearlyCountersForMeter(type, meter, catchUpTime);
652
783
 
653
784
  if (meters.length > 1) {
654
785
  await this.adapter.multiMeterManager.updateTotalCosts(type);
@@ -663,15 +794,18 @@ class BillingManager {
663
794
  if (isDecember31 && isResetTime && !needsReset) {
664
795
  // Reset at 23:59 on Dec 31 for the upcoming year
665
796
  this.adapter.log.info(`Yearly reset for ${meterBasePath} um 23:59 (Kalenderjahr)`);
666
- await this.resetYearlyCountersForMeter(type, meter);
797
+ await this.resetYearlyCountersForMeter(type, meter, getNormalizedResetTime(nowDate));
667
798
 
668
799
  if (meters.length > 1) {
669
800
  await this.adapter.multiMeterManager.updateTotalCosts(type);
670
801
  }
671
802
  } else if (needsReset) {
672
803
  // Catch-up: We're in a new year but haven't reset yet
804
+ // Use Dec 31 of last year 23:59 as reset time
805
+ const dec31LastYear = new Date(nowDate.getFullYear() - 1, 11, 31);
806
+ const catchUpTime = getNormalizedResetTime(dec31LastYear);
673
807
  this.adapter.log.info(`Yearly reset for ${meterBasePath} (Nachholung - Kalenderjahr)`);
674
- await this.resetYearlyCountersForMeter(type, meter);
808
+ await this.resetYearlyCountersForMeter(type, meter, catchUpTime);
675
809
 
676
810
  if (meters.length > 1) {
677
811
  await this.adapter.multiMeterManager.updateTotalCosts(type);
@@ -687,8 +821,9 @@ class BillingManager {
687
821
  * Resets daily counters
688
822
  *
689
823
  * @param {string} type - Utility type
824
+ * @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
690
825
  */
691
- async resetDailyCounters(type) {
826
+ async resetDailyCounters(type, resetTimestamp) {
692
827
  this.adapter.log.info(`Resetting daily counters for ${type}`);
693
828
 
694
829
  // Get all meters for this type (main + additional meters)
@@ -710,7 +845,12 @@ class BillingManager {
710
845
  const dailyValue = dailyState?.val || 0;
711
846
 
712
847
  // Save last day consumption
713
- await this.adapter.setStateAsync(`${basePath}.statistics.lastDay`, dailyValue, true);
848
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastDay`, dailyValue, true);
849
+
850
+ // Save last day costs
851
+ const dailyCostState = await this.adapter.getStateAsync(`${basePath}.costs.daily`);
852
+ const dailyCostValue = dailyCostState?.val || 0;
853
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastDay`, dailyCostValue, true);
714
854
 
715
855
  await this.adapter.setStateAsync(`${basePath}.consumption.daily`, 0, true);
716
856
 
@@ -718,7 +858,11 @@ class BillingManager {
718
858
  const dailyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.dailyVolume`);
719
859
  const dailyVolumeValue = dailyVolume?.val || 0;
720
860
  // Save last day volume for gas
721
- await this.adapter.setStateAsync(`${basePath}.statistics.lastDayVolume`, dailyVolumeValue, true);
861
+ await this.adapter.setStateAsync(
862
+ `${basePath}.statistics.consumption.lastDayVolume`,
863
+ dailyVolumeValue,
864
+ true,
865
+ );
722
866
  await this.adapter.setStateAsync(`${basePath}.consumption.dailyVolume`, 0, true);
723
867
  }
724
868
 
@@ -728,22 +872,44 @@ class BillingManager {
728
872
  if (htNtEnabled) {
729
873
  const dailyHT = await this.adapter.getStateAsync(`${basePath}.consumption.dailyHT`);
730
874
  const dailyNT = await this.adapter.getStateAsync(`${basePath}.consumption.dailyNT`);
731
- await this.adapter.setStateAsync(`${basePath}.statistics.lastDayHT`, dailyHT?.val || 0, true);
732
- await this.adapter.setStateAsync(`${basePath}.statistics.lastDayNT`, dailyNT?.val || 0, true);
875
+ await this.adapter.setStateAsync(
876
+ `${basePath}.statistics.consumption.lastDayHT`,
877
+ dailyHT?.val || 0,
878
+ true,
879
+ );
880
+ await this.adapter.setStateAsync(
881
+ `${basePath}.statistics.consumption.lastDayNT`,
882
+ dailyNT?.val || 0,
883
+ true,
884
+ );
733
885
  await this.adapter.setStateAsync(`${basePath}.consumption.dailyHT`, 0, true);
734
886
  await this.adapter.setStateAsync(`${basePath}.consumption.dailyNT`, 0, true);
887
+
888
+ // Costs for HT/NT
889
+ const dailyCostHT = (await this.adapter.getStateAsync(`${basePath}.costs.dailyHT`))?.val || 0;
890
+ const dailyCostNT = (await this.adapter.getStateAsync(`${basePath}.costs.dailyNT`))?.val || 0;
891
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastDayHT`, dailyCostHT, true);
892
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastDayNT`, dailyCostNT, true);
893
+ await this.adapter.setStateAsync(`${basePath}.costs.dailyHT`, 0, true);
894
+ await this.adapter.setStateAsync(`${basePath}.costs.dailyNT`, 0, true);
735
895
  }
736
896
 
737
897
  await this.adapter.setStateAsync(`${basePath}.costs.daily`, 0, true);
738
898
 
739
- // Update lastDayStart timestamp
740
- await this.adapter.setStateAsync(`${basePath}.statistics.lastDayStart`, Date.now(), true);
899
+ // Update lastDayStart timestamp (use normalized timestamp if provided)
900
+ const timestamp = resetTimestamp || Date.now();
901
+ await this.adapter.setStateAsync(`${basePath}.statistics.timestamps.lastDayStart`, timestamp, true);
741
902
 
742
903
  await this.adapter.setStateAsync(
743
- `${basePath}.statistics.averageDaily`,
904
+ `${basePath}.statistics.consumption.averageDaily`,
744
905
  calculator.roundToDecimals(dailyValue, 2),
745
906
  true,
746
907
  );
908
+ await this.adapter.setStateAsync(
909
+ `${basePath}.statistics.cost.averageDaily`,
910
+ calculator.roundToDecimals(dailyCostValue, 2),
911
+ true,
912
+ );
747
913
  }
748
914
 
749
915
  // Update totals if multiple meters exist
@@ -756,8 +922,9 @@ class BillingManager {
756
922
  * Resets monthly counters
757
923
  *
758
924
  * @param {string} type - Utility type
925
+ * @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
759
926
  */
760
- async resetMonthlyCounters(type) {
927
+ async resetMonthlyCounters(type, resetTimestamp) {
761
928
  this.adapter.log.info(`Resetting monthly counters for ${type}`);
762
929
 
763
930
  // Get all meters for this type (main + additional meters)
@@ -780,13 +947,22 @@ class BillingManager {
780
947
  const monthlyValue = monthlyState?.val || 0;
781
948
 
782
949
  // Save last month consumption
783
- await this.adapter.setStateAsync(`${basePath}.statistics.lastMonth`, monthlyValue, true);
950
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastMonth`, monthlyValue, true);
951
+
952
+ // Save last month costs
953
+ const monthlyCostState = await this.adapter.getStateAsync(`${basePath}.costs.monthly`);
954
+ const monthlyCostValue = monthlyCostState?.val || 0;
955
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastMonth`, monthlyCostValue, true);
784
956
 
785
957
  // For gas: also save volume
786
958
  if (type === 'gas') {
787
959
  const monthlyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.monthlyVolume`);
788
960
  const monthlyVolumeValue = monthlyVolume?.val || 0;
789
- await this.adapter.setStateAsync(`${basePath}.statistics.lastMonthVolume`, monthlyVolumeValue, true);
961
+ await this.adapter.setStateAsync(
962
+ `${basePath}.statistics.consumption.lastMonthVolume`,
963
+ monthlyVolumeValue,
964
+ true,
965
+ );
790
966
  await this.adapter.setStateAsync(`${basePath}.consumption.monthlyVolume`, 0, true);
791
967
  }
792
968
 
@@ -797,20 +973,38 @@ class BillingManager {
797
973
  const configType = getConfigType(type);
798
974
  const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
799
975
  if (htNtEnabled) {
976
+ const monthlyHT = (await this.adapter.getStateAsync(`${basePath}.consumption.monthlyHT`))?.val || 0;
977
+ const monthlyNT = (await this.adapter.getStateAsync(`${basePath}.consumption.monthlyNT`))?.val || 0;
978
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastMonthHT`, monthlyHT, true);
979
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastMonthNT`, monthlyNT, true);
800
980
  await this.adapter.setStateAsync(`${basePath}.consumption.monthlyHT`, 0, true);
801
981
  await this.adapter.setStateAsync(`${basePath}.consumption.monthlyNT`, 0, true);
982
+
983
+ // Costs for HT/NT
984
+ const monthlyCostHT = (await this.adapter.getStateAsync(`${basePath}.costs.monthlyHT`))?.val || 0;
985
+ const monthlyCostNT = (await this.adapter.getStateAsync(`${basePath}.costs.monthlyNT`))?.val || 0;
986
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastMonthHT`, monthlyCostHT, true);
987
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastMonthNT`, monthlyCostNT, true);
988
+ await this.adapter.setStateAsync(`${basePath}.costs.monthlyHT`, 0, true);
989
+ await this.adapter.setStateAsync(`${basePath}.costs.monthlyNT`, 0, true);
802
990
  }
803
991
 
804
992
  await this.adapter.setStateAsync(`${basePath}.costs.monthly`, 0, true);
805
993
 
806
- // Update lastMonthStart timestamp
807
- await this.adapter.setStateAsync(`${basePath}.statistics.lastMonthStart`, Date.now(), true);
994
+ // Update lastMonthStart timestamp (use normalized timestamp if provided)
995
+ const timestamp = resetTimestamp || Date.now();
996
+ await this.adapter.setStateAsync(`${basePath}.statistics.timestamps.lastMonthStart`, timestamp, true);
808
997
 
809
998
  await this.adapter.setStateAsync(
810
- `${basePath}.statistics.averageMonthly`,
999
+ `${basePath}.statistics.consumption.averageMonthly`,
811
1000
  calculator.roundToDecimals(monthlyValue, 2),
812
1001
  true,
813
1002
  );
1003
+ await this.adapter.setStateAsync(
1004
+ `${basePath}.statistics.cost.averageMonthly`,
1005
+ calculator.roundToDecimals(monthlyCostValue, 2),
1006
+ true,
1007
+ );
814
1008
  }
815
1009
 
816
1010
  // Update totals if multiple meters exist
@@ -842,18 +1036,51 @@ class BillingManager {
842
1036
 
843
1037
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
844
1038
 
1039
+ const yearlyState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
1040
+ const yearlyValue = yearlyState?.val || 0;
1041
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastYear`, yearlyValue, true);
1042
+
1043
+ const yearlyCostState = await this.adapter.getStateAsync(`${basePath}.costs.yearly`);
1044
+ const yearlyCostValue = yearlyCostState?.val || 0;
1045
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastYear`, yearlyCostValue, true);
1046
+
845
1047
  await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, 0, true);
846
1048
 
847
1049
  if (type === 'gas') {
1050
+ const yearlyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.yearlyVolume`);
1051
+ const yearlyVolumeValue = yearlyVolume?.val || 0;
1052
+ await this.adapter.setStateAsync(
1053
+ `${basePath}.statistics.consumption.lastYearVolume`,
1054
+ yearlyVolumeValue,
1055
+ true,
1056
+ );
848
1057
  await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, 0, true);
849
1058
  }
850
1059
 
1060
+ const configType = getConfigType(type);
1061
+ const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
1062
+ if (htNtEnabled) {
1063
+ const yearlyHT = (await this.adapter.getStateAsync(`${basePath}.consumption.yearlyHT`))?.val || 0;
1064
+ const yearlyNT = (await this.adapter.getStateAsync(`${basePath}.consumption.yearlyNT`))?.val || 0;
1065
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastYearHT`, yearlyHT, true);
1066
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastYearNT`, yearlyNT, true);
1067
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyHT`, 0, true);
1068
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyNT`, 0, true);
1069
+
1070
+ const yearlyCostHT = (await this.adapter.getStateAsync(`${basePath}.costs.yearlyHT`))?.val || 0;
1071
+ const yearlyCostNT = (await this.adapter.getStateAsync(`${basePath}.costs.yearlyNT`))?.val || 0;
1072
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastYearHT`, yearlyCostHT, true);
1073
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastYearNT`, yearlyCostNT, true);
1074
+ await this.adapter.setStateAsync(`${basePath}.costs.yearlyHT`, 0, true);
1075
+ await this.adapter.setStateAsync(`${basePath}.costs.yearlyNT`, 0, true);
1076
+ }
1077
+
851
1078
  await this.adapter.setStateAsync(`${basePath}.costs.yearly`, 0, true);
852
1079
  await this.adapter.setStateAsync(`${basePath}.billing.notificationSent`, false, true);
853
1080
  await this.adapter.setStateAsync(`${basePath}.billing.notificationChangeSent`, false, true);
854
1081
 
855
1082
  // Update lastYearStart timestamp
856
- await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, Date.now(), true);
1083
+ await this.adapter.setStateAsync(`${basePath}.statistics.timestamps.lastYearStart`, Date.now(), true);
857
1084
  }
858
1085
 
859
1086
  // Update totals if multiple meters exist
@@ -867,33 +1094,69 @@ class BillingManager {
867
1094
  *
868
1095
  * @param {string} type - Utility type
869
1096
  * @param {object} meter - Meter object from multiMeterManager
1097
+ * @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
870
1098
  */
871
- async resetYearlyCountersForMeter(type, meter) {
1099
+ async resetYearlyCountersForMeter(type, meter, resetTimestamp) {
872
1100
  const basePath = `${type}.${meter.name}`;
873
1101
  const label = meter.displayName || meter.name;
874
1102
 
875
1103
  this.adapter.log.debug(`Resetting yearly counter for ${basePath} (${label})`);
876
1104
 
1105
+ const yearlyState = await this.adapter.getStateAsync(`${basePath}.consumption.yearly`);
1106
+ const yearlyValue = yearlyState?.val || 0;
1107
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastYear`, yearlyValue, true);
1108
+
1109
+ const yearlyCostState = await this.adapter.getStateAsync(`${basePath}.costs.yearly`);
1110
+ const yearlyCostValue = yearlyCostState?.val || 0;
1111
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastYear`, yearlyCostValue, true);
1112
+
877
1113
  await this.adapter.setStateAsync(`${basePath}.consumption.yearly`, 0, true);
878
1114
 
879
1115
  if (type === 'gas') {
1116
+ const yearlyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.yearlyVolume`);
1117
+ const yearlyVolumeValue = yearlyVolume?.val || 0;
1118
+ await this.adapter.setStateAsync(
1119
+ `${basePath}.statistics.consumption.lastYearVolume`,
1120
+ yearlyVolumeValue,
1121
+ true,
1122
+ );
880
1123
  await this.adapter.setStateAsync(`${basePath}.consumption.yearlyVolume`, 0, true);
881
1124
  }
882
1125
 
1126
+ const configType = getConfigType(type);
1127
+ const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
1128
+ if (htNtEnabled) {
1129
+ const yearlyHT = (await this.adapter.getStateAsync(`${basePath}.consumption.yearlyHT`))?.val || 0;
1130
+ const yearlyNT = (await this.adapter.getStateAsync(`${basePath}.consumption.yearlyNT`))?.val || 0;
1131
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastYearHT`, yearlyHT, true);
1132
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastYearNT`, yearlyNT, true);
1133
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyHT`, 0, true);
1134
+ await this.adapter.setStateAsync(`${basePath}.consumption.yearlyNT`, 0, true);
1135
+
1136
+ const yearlyCostHT = (await this.adapter.getStateAsync(`${basePath}.costs.yearlyHT`))?.val || 0;
1137
+ const yearlyCostNT = (await this.adapter.getStateAsync(`${basePath}.costs.yearlyNT`))?.val || 0;
1138
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastYearHT`, yearlyCostHT, true);
1139
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastYearNT`, yearlyCostNT, true);
1140
+ await this.adapter.setStateAsync(`${basePath}.costs.yearlyHT`, 0, true);
1141
+ await this.adapter.setStateAsync(`${basePath}.costs.yearlyNT`, 0, true);
1142
+ }
1143
+
883
1144
  await this.adapter.setStateAsync(`${basePath}.costs.yearly`, 0, true);
884
1145
  await this.adapter.setStateAsync(`${basePath}.billing.notificationSent`, false, true);
885
1146
  await this.adapter.setStateAsync(`${basePath}.billing.notificationChangeSent`, false, true);
886
1147
 
887
- // Update lastYearStart timestamp
888
- await this.adapter.setStateAsync(`${basePath}.statistics.lastYearStart`, Date.now(), true);
1148
+ // Update lastYearStart timestamp (use normalized timestamp if provided)
1149
+ const timestamp = resetTimestamp || Date.now();
1150
+ await this.adapter.setStateAsync(`${basePath}.statistics.timestamps.lastYearStart`, timestamp, true);
889
1151
  }
890
1152
 
891
1153
  /**
892
1154
  * Resets weekly counters
893
1155
  *
894
1156
  * @param {string} type - Utility type
1157
+ * @param {number} [resetTimestamp] - Optional normalized timestamp (defaults to Date.now())
895
1158
  */
896
- async resetWeeklyCounters(type) {
1159
+ async resetWeeklyCounters(type, resetTimestamp) {
897
1160
  this.adapter.log.info(`Resetting weekly counters for ${type}`);
898
1161
 
899
1162
  const meters = this.adapter.multiMeterManager?.getMetersForType(type) || [];
@@ -903,13 +1166,22 @@ class BillingManager {
903
1166
  // Save last week consumption before reset
904
1167
  const weeklyState = await this.adapter.getStateAsync(`${basePath}.consumption.weekly`);
905
1168
  const weeklyValue = weeklyState?.val || 0;
906
- await this.adapter.setStateAsync(`${basePath}.statistics.lastWeek`, weeklyValue, true);
1169
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastWeek`, weeklyValue, true);
1170
+
1171
+ // Save last week costs before reset
1172
+ const weeklyCostState = await this.adapter.getStateAsync(`${basePath}.costs.weekly`);
1173
+ const weeklyCostValue = weeklyCostState?.val || 0;
1174
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastWeek`, weeklyCostValue, true);
907
1175
 
908
1176
  // For gas: also save volume
909
1177
  if (type === 'gas') {
910
1178
  const weeklyVolume = await this.adapter.getStateAsync(`${basePath}.consumption.weeklyVolume`);
911
1179
  const weeklyVolumeValue = weeklyVolume?.val || 0;
912
- await this.adapter.setStateAsync(`${basePath}.statistics.lastWeekVolume`, weeklyVolumeValue, true);
1180
+ await this.adapter.setStateAsync(
1181
+ `${basePath}.statistics.consumption.lastWeekVolume`,
1182
+ weeklyVolumeValue,
1183
+ true,
1184
+ );
913
1185
  await this.adapter.setStateAsync(`${basePath}.consumption.weeklyVolume`, 0, true);
914
1186
  }
915
1187
 
@@ -920,13 +1192,25 @@ class BillingManager {
920
1192
  const configType = getConfigType(type);
921
1193
  const htNtEnabled = this.adapter.config[`${configType}HtNtEnabled`] || false;
922
1194
  if (htNtEnabled) {
1195
+ const weeklyHT = (await this.adapter.getStateAsync(`${basePath}.consumption.weeklyHT`))?.val || 0;
1196
+ const weeklyNT = (await this.adapter.getStateAsync(`${basePath}.consumption.weeklyNT`))?.val || 0;
1197
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastWeekHT`, weeklyHT, true);
1198
+ await this.adapter.setStateAsync(`${basePath}.statistics.consumption.lastWeekNT`, weeklyNT, true);
923
1199
  await this.adapter.setStateAsync(`${basePath}.consumption.weeklyHT`, 0, true);
924
1200
  await this.adapter.setStateAsync(`${basePath}.consumption.weeklyNT`, 0, true);
1201
+
1202
+ // Costs for HT/NT
1203
+ const weeklyCostHT = (await this.adapter.getStateAsync(`${basePath}.costs.weeklyHT`))?.val || 0;
1204
+ const weeklyCostNT = (await this.adapter.getStateAsync(`${basePath}.costs.weeklyNT`))?.val || 0;
1205
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastWeekHT`, weeklyCostHT, true);
1206
+ await this.adapter.setStateAsync(`${basePath}.statistics.cost.lastWeekNT`, weeklyCostNT, true);
925
1207
  await this.adapter.setStateAsync(`${basePath}.costs.weeklyHT`, 0, true);
926
1208
  await this.adapter.setStateAsync(`${basePath}.costs.weeklyNT`, 0, true);
927
1209
  }
928
1210
 
929
- await this.adapter.setStateAsync(`${basePath}.statistics.lastWeekStart`, Date.now(), true);
1211
+ // Update lastWeekStart timestamp (use normalized timestamp if provided)
1212
+ const timestamp = resetTimestamp || Date.now();
1213
+ await this.adapter.setStateAsync(`${basePath}.statistics.timestamps.lastWeekStart`, timestamp, true);
930
1214
  }
931
1215
 
932
1216
  if (meters.length > 1) {