|
1 | 1 | // Package mpc – cell-balancing feature tests. |
2 | 2 | // |
3 | 3 | // These tests cover: |
4 | | -// 1. needsDailyBalancing – the once-per-day guard |
| 4 | +// 1. needsWeeklyBalancing – the once-per-week guard |
5 | 5 | // 2. CV-phase energy modelling – reduced efficiency near BatteryMaxSOC |
6 | 6 | // 3. Optimizer integration – bonus incentivises full charge, no double-counting |
7 | 7 | package mpc |
@@ -35,9 +35,9 @@ func balancingConfig() SystemConfig { |
35 | 35 | } |
36 | 36 | } |
37 | 37 |
|
38 | | -// Anchor timestamps at noon UTC on two consecutive days. |
39 | | -// Using noon (12:00 UTC) keeps the calendar-day comparison in |
40 | | -// needsDailyBalancing consistent across all populated timezones (UTC-11 … UTC+11). |
| 38 | +// Anchor timestamps at noon UTC. |
| 39 | +// Using noon (12:00 UTC) keeps time-based comparisons in needsWeeklyBalancing |
| 40 | +// consistent across all populated timezones (UTC-11 … UTC+11). |
41 | 41 | // |
42 | 42 | // 2024-01-04 00:00:00 UTC = 1704326400 (used by existing tests) |
43 | 43 | // 2024-01-04 12:00:00 UTC = 1704326400 + 12*3600 = 1704369600 |
@@ -73,52 +73,53 @@ func expensiveBalancingSlot(ts int64, hour int) TimeSlot { |
73 | 73 | } |
74 | 74 |
|
75 | 75 | // ─── needsDailyBalancing ───────────────────────────────────────────────────── |
| 76 | +// ─── needsWeeklyBalancing ────────────────────────────────────────────────────────────────────── |
76 | 77 |
|
77 | | -func TestNeedsDailyBalancing_FeatureDisabled(t *testing.T) { |
| 78 | +func TestNeedsWeeklyBalancing_FeatureDisabled(t *testing.T) { |
78 | 79 | config := balancingConfig() |
79 | 80 | config.BatteryBalancingBonus = 0 // feature off |
80 | 81 |
|
81 | 82 | ctrl := NewController(config, 1, 0.5) |
82 | 83 | ctrl.LastBalancingTime = 0 // never balanced |
83 | 84 |
|
84 | | - if ctrl.needsDailyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
85 | | - t.Error("needsDailyBalancing must return false when BatteryBalancingBonus is 0") |
| 85 | + if ctrl.needsWeeklyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
| 86 | + t.Error("needsWeeklyBalancing must return false when BatteryBalancingBonus is 0") |
86 | 87 | } |
87 | 88 | } |
88 | 89 |
|
89 | | -func TestNeedsDailyBalancing_NeverBalanced(t *testing.T) { |
| 90 | +func TestNeedsWeeklyBalancing_NeverBalanced(t *testing.T) { |
90 | 91 | ctrl := NewController(balancingConfig(), 1, 0.5) |
91 | 92 | ctrl.LastBalancingTime = 0 // zero means "never" |
92 | 93 |
|
93 | | - if !ctrl.needsDailyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
94 | | - t.Error("needsDailyBalancing must return true when the battery has never been fully charged") |
| 94 | + if !ctrl.needsWeeklyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
| 95 | + t.Error("needsWeeklyBalancing must return true when the battery has never been fully charged") |
95 | 96 | } |
96 | 97 | } |
97 | 98 |
|
98 | | -func TestNeedsDailyBalancing_AlreadyBalancedToday(t *testing.T) { |
| 99 | +func TestNeedsWeeklyBalancing_AlreadyBalancedWithinAWeek(t *testing.T) { |
99 | 100 | ctrl := NewController(balancingConfig(), 1, 0.5) |
100 | | - ctrl.LastBalancingTime = jan05noon - 2*3600 // 2 h before forecast start, same calendar day |
| 101 | + ctrl.LastBalancingTime = jan05noon - 6*24*3600 // 6 days before forecast start |
101 | 102 |
|
102 | | - if ctrl.needsDailyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
103 | | - t.Error("needsDailyBalancing must return false when balancing already happened today") |
| 103 | + if ctrl.needsWeeklyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
| 104 | + t.Error("needsWeeklyBalancing must return false when balancing happened less than 7 days ago") |
104 | 105 | } |
105 | 106 | } |
106 | 107 |
|
107 | | -func TestNeedsDailyBalancing_BalancedYesterday(t *testing.T) { |
| 108 | +func TestNeedsWeeklyBalancing_BalancedExactlyAWeekAgo(t *testing.T) { |
108 | 109 | ctrl := NewController(balancingConfig(), 1, 0.5) |
109 | | - ctrl.LastBalancingTime = jan04noon // previous calendar day (24 h before jan05noon) |
| 110 | + ctrl.LastBalancingTime = jan05noon - 7*24*3600 // exactly 7 days before forecast start |
110 | 111 |
|
111 | | - if !ctrl.needsDailyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
112 | | - t.Error("needsDailyBalancing must return true when last balancing was on a previous day") |
| 112 | + if !ctrl.needsWeeklyBalancing([]TimeSlot{{Timestamp: jan05noon}}) { |
| 113 | + t.Error("needsWeeklyBalancing must return true when at least 7 days have elapsed since last balancing") |
113 | 114 | } |
114 | 115 | } |
115 | 116 |
|
116 | | -func TestNeedsDailyBalancing_EmptyForecast(t *testing.T) { |
| 117 | +func TestNeedsWeeklyBalancing_EmptyForecast(t *testing.T) { |
117 | 118 | ctrl := NewController(balancingConfig(), 0, 0.5) |
118 | 119 | ctrl.LastBalancingTime = 0 |
119 | 120 |
|
120 | | - if ctrl.needsDailyBalancing([]TimeSlot{}) { |
121 | | - t.Error("needsDailyBalancing must return false for an empty forecast") |
| 121 | + if ctrl.needsWeeklyBalancing([]TimeSlot{}) { |
| 122 | + t.Error("needsWeeklyBalancing must return false for an empty forecast") |
122 | 123 | } |
123 | 124 | } |
124 | 125 |
|
@@ -321,9 +322,9 @@ func TestOptimizer_BalancingSkippedWhenAlreadyDoneToday(t *testing.T) { |
321 | 322 | ctrlA.LastBalancingTime = 0 |
322 | 323 | decisionsA := ctrlA.Optimize(forecast) |
323 | 324 |
|
324 | | - // Run B: balancing already done today → incentive suppressed |
| 325 | + // Run B: balancing done recently (within 7 days) → incentive suppressed |
325 | 326 | ctrlB := NewController(config, len(forecast), 0.90) |
326 | | - ctrlB.LastBalancingTime = jan05noon - 3600 // 1 h before the forecast, same calendar day |
| 327 | + ctrlB.LastBalancingTime = jan05noon - 3600 // 1 h before the forecast start |
327 | 328 | decisionsB := ctrlB.Optimize(forecast) |
328 | 329 |
|
329 | 330 | peakA, peakB := 0.0, 0.0 |
|
0 commit comments