Optimizer: fix timestamp generation (#28677)

This commit is contained in:
RenatusRo 2026-04-03 16:52:13 +02:00 • committed by GitHub
parent 764aa45d8c
commit bb07ec0d4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 45 additions and 23 deletions

View file

@ -218,9 +218,9 @@ export default defineComponent({
type: Array as PropType<BatteryDetail[]>,
required: true,
},
timestamp: {
type: String,
default: "",
timestamps: {
type: Array as PropType<string[]>,
default: () => [],
},
currency: {
type: String as PropType<CURRENCY>,
@ -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];

View file

@ -82,7 +82,7 @@
<TimeSeriesDataTable
:evopt="evopt"
:battery-details="evopt.details.batteryDetails"
:timestamp="evopt.details.timestamp[0]"
:timestamps="evopt.details.timestamp"
:currency="currency"
:battery-colors="batteryColors"
:dimmed-battery-colors="dimmedBatteryColors"

View file

@ -144,7 +144,8 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error {
return fmt.Errorf("not enough slots for optimization: %d (grid=%d, feedIn=%d, solar=%d)", minLen, len(grid), len(feedIn), len(solar))
}
dt := timeSteps(minLen)
now := time.Now()
dt := timeSteps(minLen, now)
firstSlotDuration := time.Duration(dt[0]) * time.Second
site.log.DEBUG.Printf("optimizer: optimizing %d slots until %v: grid=%d, feedIn=%d, solar=%d, first slot: %v",
@ -190,7 +191,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error {
pa := lo.Min(req.TimeSeries.PN) * eta * 0.99
details := requestDetails{
Timestamps: asTimestamps(dt),
Timestamps: asTimestamps(dt, now),
}
if site.circuit != nil {
@ -588,10 +589,6 @@ func solarRatesToEnergy(rr api.Rates) (api.Rates, error) {
return res, nil
}
func endOfHour(ts time.Time) time.Time {
return ts.Truncate(time.Hour).Add(time.Hour)
}
func currentRates(tariff api.Tariff) api.Rates {
if tariff == nil {
return nil
@ -609,12 +606,11 @@ func currentRates(tariff api.Tariff) api.Rates {
})
}
func timeSteps(minLen int) []int {
func timeSteps(minLen int, now time.Time) []int {
res := make([]int, 0, minLen)
bos := time.Now().Truncate(tariff.SlotDuration)
eos := bos.Add(tariff.SlotDuration)
if d := time.Until(eos); d > 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
}

View file

@ -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)