Optimizer: respect session energy limit when vehicle soc is unknown (#32895)
This commit is contained in:
parent
24498ad1f6
commit
bb6854b1ed
5 changed files with 84 additions and 11 deletions
|
|
@ -219,6 +219,8 @@ type API interface {
|
|||
GetRemainingDuration() time.Duration
|
||||
// GetRemainingEnergy is the remaining charge energy in kWh
|
||||
GetRemainingEnergy() float64
|
||||
// GetChargedEnergy returns session charge energy in Wh
|
||||
GetChargedEnergy() float64
|
||||
|
||||
//
|
||||
// vehicles
|
||||
|
|
|
|||
|
|
@ -221,6 +221,20 @@ func (mr *MockAPIMockRecorder) GetChargePowerFlexibility(rates any) *gomock.Call
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility), rates)
|
||||
}
|
||||
|
||||
// GetChargedEnergy mocks base method.
|
||||
func (m *MockAPI) GetChargedEnergy() float64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChargedEnergy")
|
||||
ret0, _ := ret[0].(float64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetChargedEnergy indicates an expected call of GetChargedEnergy.
|
||||
func (mr *MockAPIMockRecorder) GetChargedEnergy() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargedEnergy", reflect.TypeOf((*MockAPI)(nil).GetChargedEnergy))
|
||||
}
|
||||
|
||||
// GetChargerRef mocks base method.
|
||||
func (m *MockAPI) GetChargerRef() string {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
|||
|
|
@ -47,3 +47,7 @@ Use minimum of energy consumption cost.
|
|||
- home battery or loadpoint/vehicle...
|
||||
- capacity, soc and charge goals
|
||||
- charge/discharge power limits and efficiency
|
||||
|
||||
Without vehicle capacity or soc a configured session energy limit is modelled instead:
|
||||
state is the session's charged energy, goal is the limit. Loadpoints with neither are
|
||||
not modelled at all- their power is added to the base load.
|
||||
|
|
|
|||
|
|
@ -522,8 +522,9 @@ func (site *Site) optimizerRequest(battery []types.Measurement) (optimizer.Optim
|
|||
continue
|
||||
}
|
||||
|
||||
// unknown vehicle capacity: account for the consumption as uncontrollable load
|
||||
if v := lp.GetVehicle(); v == nil || v.Capacity() == 0 {
|
||||
// no vehicle capacity and no session energy limit to model against:
|
||||
// account for the consumption as uncontrollable load
|
||||
if v := lp.GetVehicle(); v == nil || (v.Capacity() == 0 && lp.GetLimitEnergy() == 0) {
|
||||
unmodelled += unmodelledPower(lp)
|
||||
continue
|
||||
}
|
||||
|
|
@ -790,18 +791,22 @@ func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDurati
|
|||
// vehicle
|
||||
v := lp.GetVehicle()
|
||||
|
||||
maxSoc := v.Capacity() * 1e3 // Wh
|
||||
if v := lp.EffectiveLimitSoc(); v > 0 {
|
||||
maxSoc *= float64(v) / 100
|
||||
} else if v := lp.GetLimitEnergy(); v > 0 {
|
||||
maxSoc = v * 1e3
|
||||
capacity := v.Capacity() // kWh
|
||||
soc := lp.GetSoc() // percent
|
||||
|
||||
// without capacity or soc there is no battery state to model, but a session energy
|
||||
// limit still bounds the charge- use charged energy as state (see remainingLimitEnergy)
|
||||
if limit := lp.GetLimitEnergy(); limit > 0 && (capacity == 0 || soc == 0) {
|
||||
bat.SInitial = float32(lp.GetChargedEnergy()) // Wh
|
||||
bat.SMax = max(bat.SInitial, float32(limit*1e3)) // prevent infeasible if limit already exceeded
|
||||
} else {
|
||||
maxSoc := capacity * float64(lp.EffectiveLimitSoc()) * 10 // Wh
|
||||
bat.SInitial = float32(capacity * soc * 10) // Wh
|
||||
bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum
|
||||
}
|
||||
|
||||
bat.SInitial = float32(v.Capacity() * lp.GetSoc() * 10) // Wh
|
||||
bat.SMax = max(bat.SInitial, float32(maxSoc)) // prevent infeasible if current soc above maximum
|
||||
|
||||
detail.Type = batteryTypeVehicle
|
||||
detail.Capacity = v.Capacity()
|
||||
detail.Capacity = capacity
|
||||
|
||||
if vt := v.GetTitle(); vt != "" {
|
||||
if detail.Title != "" {
|
||||
|
|
|
|||
|
|
@ -320,6 +320,54 @@ func TestBatteryRequestSocLimitsClamp(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// charge goal for vehicles with and without known capacity/soc, see #32890
|
||||
func TestLoadpointRequestChargeGoal(t *testing.T) {
|
||||
site := &Site{log: util.NewLogger("foo")}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
capacity, soc float64 // kWh, percent
|
||||
limitSoc int // percent
|
||||
limitEnergy, charged float64 // kWh, Wh
|
||||
wantInitial, wantSMax float32 // Wh
|
||||
}{
|
||||
{"soc limit", 50, 20, 80, 0, 0, 10000, 40000},
|
||||
{"no capacity, energy limit", 0, 0, 100, 10, 0, 0, 10000},
|
||||
{"no capacity, energy limit partially charged", 0, 0, 100, 10, 4000, 4000, 10000},
|
||||
{"no capacity, limit exceeded", 0, 0, 100, 10, 11000, 11000, 11000},
|
||||
{"capacity but no soc, energy limit", 50, 0, 100, 10, 0, 0, 10000},
|
||||
{"capacity but no soc, no energy limit", 50, 0, 100, 0, 0, 0, 50000},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
v := api.NewMockVehicle(ctrl)
|
||||
v.EXPECT().Capacity().Return(tc.capacity).AnyTimes()
|
||||
v.EXPECT().GetTitle().Return("").AnyTimes()
|
||||
|
||||
lp := loadpoint.NewMockAPI(ctrl)
|
||||
lp.EXPECT().GetVehicle().Return(v).AnyTimes()
|
||||
lp.EXPECT().GetSoc().Return(tc.soc).AnyTimes()
|
||||
lp.EXPECT().EffectiveLimitSoc().Return(tc.limitSoc).AnyTimes()
|
||||
lp.EXPECT().GetLimitEnergy().Return(tc.limitEnergy).AnyTimes()
|
||||
lp.EXPECT().GetChargedEnergy().Return(tc.charged).AnyTimes()
|
||||
lp.EXPECT().GetTitle().Return("lp").AnyTimes()
|
||||
lp.EXPECT().EffectiveMinPower().Return(1380.0).AnyTimes()
|
||||
lp.EXPECT().EffectiveMaxPower().Return(11000.0).AnyTimes()
|
||||
lp.EXPECT().GetMode().Return(api.ModePV).AnyTimes()
|
||||
lp.EXPECT().GetStatus().Return(api.StatusB).AnyTimes()
|
||||
lp.EXPECT().GetSmartCostLimit().Return(nil).AnyTimes()
|
||||
lp.EXPECT().EffectivePlanStrategy().Return(api.PlanStrategy{}).AnyTimes()
|
||||
lp.EXPECT().GetPlanGoal().Return(0.0, false).AnyTimes()
|
||||
|
||||
req, _ := site.loadpointRequest(lp, 8, 15*time.Minute, nil)
|
||||
|
||||
assert.Equal(t, tc.wantInitial, req.SInitial)
|
||||
assert.Equal(t, tc.wantSMax, req.SMax)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptimizerChargingStrategy(t *testing.T) {
|
||||
site := &Site{log: util.NewLogger("foo")}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue