Optimizer: consider planner precondition (late charging) (#32872)

This commit is contained in:
andig 2026-08-16 09:52:52 +02:00 • committed by GitHub
parent 4f169c5812
commit 0bbf4dc5c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 2 deletions

View file

@ -832,13 +832,15 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati
case api.ModeMinPV:
// forced min charging
demand = continuousDemand(lp, minLen)
// add smartcost limit and plan goal, if configured
// add smartcost limit, precondition and plan goal, if configured
demand = applySmartCostLimit(lp, demand, grid, minLen)
demand = applyPrecondition(lp, demand, minLen)
site.applyPlanGoal(lp, &bat, minLen)
case api.ModePV:
// add smartcost limit and plan goal, if configured
// add smartcost limit, precondition and plan goal, if configured
demand = applySmartCostLimit(lp, nil, grid, minLen)
demand = applyPrecondition(lp, demand, minLen)
site.applyPlanGoal(lp, &bat, minLen)
}
@ -1222,6 +1224,50 @@ func applySmartCostLimit(lp loadpoint.API, demand []float32, grid api.Rates, min
return demand
}
// applyPrecondition forces max charging power during the planner's precondition window
// ("late charging"), i.e. the last precondition duration before the plan time
func applyPrecondition(lp loadpoint.API, demand []float32, minLen int) []float32 {
precondition := lp.EffectivePlanStrategy().Precondition
if precondition <= 0 {
return demand
}
ts := lp.EffectivePlanTime()
if ts.IsZero() {
return demand
}
// TODO precise slot placement
end := time.Until(ts)
start := end - precondition
if end <= 0 {
return demand
}
first := max(int(start/tariff.SlotDuration), 0)
if first >= minLen {
return demand
}
if demand == nil {
demand = make([]float32, minLen)
}
energy := float32(lp.EffectiveMaxPower() / slotsPerHour)
for i := first; i < minLen; i++ {
slotStart := time.Duration(i) * tariff.SlotDuration
overlap := min(end, slotStart+tariff.SlotDuration) - max(start, slotStart)
if overlap <= 0 {
break
}
demand[i] = max(demand[i], energy*float32(overlap)/float32(tariff.SlotDuration))
}
return demand
}
func (site *Site) applyBatteryGridChargeLimit(cMax float32, grid api.Rates, minLen int) []float32 {
limit := site.GetBatteryGridChargeLimit()
if limit == nil {

View file

@ -29,6 +29,40 @@ func TestLoadpointProfile(t *testing.T) {
require.Equal(t, []float64{250, 250, 250, 250, 250, 250, 250, 50}, loadpointProfile(lp, 8))
}
func TestApplyPrecondition(t *testing.T) {
ctrl := gomock.NewController(t)
lp := loadpoint.NewMockAPI(ctrl)
lp.EXPECT().EffectiveMaxPower().Return(8000.0).AnyTimes() // 2 kWh per slot
// no precondition configured
lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{}).Times(1)
assert.Nil(t, applyPrecondition(lp, nil, 8))
// no plan
lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{Precondition: time.Hour}).Times(1)
lp.EXPECT().EffectivePlanTime().Return(time.Time{}).Times(1)
assert.Nil(t, applyPrecondition(lp, nil, 8))
// plan in 1h, 40min precondition: slots 1 (10min) and 2, 3 (full)
lp.EXPECT().EffectivePlanTime().Return(time.Now().Add(time.Hour)).Times(1)
lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{Precondition: 40 * time.Minute}).Times(1)
res := applyPrecondition(lp, nil, 8)
require.Len(t, res, 8)
assert.InDeltaSlice(t, []float32{0, 2000. / 1.5, 2000, 2000, 0, 0, 0, 0}, res, 1)
// existing demand is kept where higher
lp.EXPECT().EffectivePlanTime().Return(time.Now().Add(time.Hour)).Times(1)
lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{Precondition: 30 * time.Minute}).Times(1)
res = applyPrecondition(lp, []float32{3000, 3000, 3000, 3000, 0, 0, 0, 0}, 8)
assert.InDeltaSlice(t, []float32{3000, 3000, 3000, 3000, 0, 0, 0, 0}, res, 1)
// plan beyond horizon
lp.EXPECT().EffectivePlanTime().Return(time.Now().Add(24 * time.Hour)).Times(1)
lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{Precondition: time.Hour}).Times(1)
assert.Nil(t, applyPrecondition(lp, nil, 8))
}
func TestLoadpointCurrentAction(t *testing.T) {
for _, tc := range []struct {
name string