diff --git a/assets/js/components/Optimize/TimeSeriesDataTable.vue b/assets/js/components/Optimize/TimeSeriesDataTable.vue index 796c96a97..2bf0d63d6 100644 --- a/assets/js/components/Optimize/TimeSeriesDataTable.vue +++ b/assets/js/components/Optimize/TimeSeriesDataTable.vue @@ -218,9 +218,9 @@ export default defineComponent({ type: Array as PropType, required: true, }, - timestamp: { - type: String, - default: "", + timestamps: { + type: Array as PropType, + default: () => [], }, currency: { type: String as PropType, @@ -262,13 +262,16 @@ export default defineComponent({ return (seconds / 3600).toFixed(2); }, formatHour(index: number): string { - // Show label only every 4th slot (every hour for 15-minute slots) - if (index % 4 !== 0) { + if (this.timestamps.length <= index) { return ""; } - const startTime = new Date(this.timestamp); - const currentTime = new Date(startTime.getTime() + index * 15 * 60 * 1000); // Add 15-minute intervals - return currentTime.getHours().toString(); + // Show label on fresh hour or on first quarter hour for slot 0 + const ts = new Date(this.timestamps[index]); + const minutes = ts.getMinutes(); + if (minutes === 0 || (index === 0 && minutes < 15)) { + return ts.getHours().toString(); + } + return ""; }, getBatteryTitle(index: number): string { const detail = this.batteryDetails[index]; diff --git a/assets/js/views/Optimize.vue b/assets/js/views/Optimize.vue index 3a133b713..77126ec81 100644 --- a/assets/js/views/Optimize.vue +++ b/assets/js/views/Optimize.vue @@ -82,7 +82,7 @@ time.Second && d < tariff.SlotDuration { + eos := now.Truncate(tariff.SlotDuration).Add(tariff.SlotDuration) + if d := eos.Sub(now); d > time.Second && d < tariff.SlotDuration { res = append(res, int(d.Seconds())) } @@ -625,13 +621,16 @@ func timeSteps(minLen int) []int { return res } -func asTimestamps(dt []int) []time.Time { +func asTimestamps(dt []int, now time.Time) []time.Time { res := make([]time.Time, 0, len(dt)) - eoh := endOfHour(time.Now()) - res = append(res, eoh.Add(-time.Duration(dt[0])*time.Second)) + + eos := now.Truncate(tariff.SlotDuration).Add(tariff.SlotDuration) + res = append(res, eos.Add(-time.Duration(dt[0])*time.Second)) + for i := range len(dt) - 1 { - res = append(res, eoh.Add(time.Duration(dt[i+1])*time.Second)) + res = append(res, res[i].Add(time.Duration(dt[i])*time.Second)) } + return res } diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 70fda2362..ca8e86146 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -111,6 +111,26 @@ func TestLoadpointProfile(t *testing.T) { require.Equal(t, []float64{250, 250, 250, 250, 250, 250, 250, 50}, loadpointProfile(lp, 8)) } +func TestAsTimestamps(t *testing.T) { + // now is 10 minutes into a 15-minute slot + now := time.Date(2025, 1, 1, 12, 10, 0, 0, time.UTC) + + // dt[0]=300 means first event is 300s (5min) before end of current slot + // dt[1..] just mark subsequent slot boundaries + dt := []int{60 * 5, 60 * 15, 60 * 15} + + got := asTimestamps(dt, now) + + // current slot: 12:00–12:15 + // first timestamp: 12:15 - 5min = 12:10 + // subsequent: 12:15, 12:30 + assert.Equal(t, []time.Time{ + time.Date(2025, 1, 1, 12, 10, 0, 0, time.UTC), + time.Date(2025, 1, 1, 12, 15, 0, 0, time.UTC), + time.Date(2025, 1, 1, 12, 30, 0, 0, time.UTC), + }, got) +} + func TestBatteryForecastTotals(t *testing.T) { site := new(Site)