Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loadpoint: add battery boost (experimental) #16599

Merged
merged 25 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/keys/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
DisableThreshold = "disableThreshold"
EnableDelay = "enableDelay"
DisableDelay = "disableDelay"
BatteryBoost = "batteryBoost"

PhasesConfigured = "phasesConfigured" // configured phases (1/3, 0 for auto on 1p3p chargers, nil for plain chargers)
PhasesEnabled = "phasesEnabled" // enabled phases (1/3)
Expand Down
57 changes: 53 additions & 4 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const (

chargerSwitchDuration = 60 * time.Second // allow out of sync during this timespan
phaseSwitchDuration = 60 * time.Second // allow out of sync and do not measure phases during this timespan

// battery boost states
boostDisabled = 0
boostStart = 1
boostContinue = 2
)

// elapsed is the time an expired timer will be set to
Expand Down Expand Up @@ -127,6 +132,7 @@ type Loadpoint struct {
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
smartCostLimit *float64 // always charge if cost is below this value
batteryBoost int // battery boost state

mode api.ChargeMode
enabled bool // Charger enabled state
Expand Down Expand Up @@ -531,6 +537,11 @@ func (lp *Loadpoint) evVehicleDisconnectHandler() {
// soc update reset
lp.socUpdated = time.Time{}

// boost
if err := lp.SetBatteryBoost(false); err != nil {
lp.log.ERROR.Printf("battery boost: %v", err)
}

// reset session
lp.SetLimitSoc(0)
lp.SetLimitEnergy(0)
Expand Down Expand Up @@ -800,10 +811,16 @@ func (lp *Loadpoint) syncCharger() error {
return nil
}

// coarseCurrent returns true if charger or vehicle require full amp steps
func (lp *Loadpoint) coarseCurrent() bool {
_, ok := lp.charger.(api.ChargerEx)
return !ok || lp.vehicleHasFeature(api.CoarseCurrent)
}

// roundedCurrent rounds current down to full amps if charger or vehicle require it
func (lp *Loadpoint) roundedCurrent(chargeCurrent float64) float64 {
// full amps only?
if _, ok := lp.charger.(api.ChargerEx); !ok || lp.vehicleHasFeature(api.CoarseCurrent) {
if lp.coarseCurrent() {
chargeCurrent = math.Trunc(chargeCurrent)
}
return chargeCurrent
Expand Down Expand Up @@ -1273,12 +1290,44 @@ func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action strin
}
}

// boostPower returns the additional power that the loadpoint should draw from the battery
func (lp *Loadpoint) boostPower(batteryBoostPower float64, batteryBuffered bool) float64 {
boost := lp.getBatteryBoost()
if boost == boostDisabled || !batteryBuffered {
return 0
}

// push demand to drain battery
delta := lp.effectiveStepPower()

// start boosting by setting maximum power
if boost == boostStart {
delta = lp.EffectiveMaxPower()

// expire timers
lp.phaseTimer = elapsed
lp.pvTimer = elapsed

if lp.charging() {
lp.setBatteryBoost(boostContinue)
}
}

res := batteryBoostPower + delta
lp.log.DEBUG.Printf("pv charge battery boost: %.0fW = -%.0fW battery - %.0fW boost", -res, batteryBoostPower, delta)

return res
}

// pvMaxCurrent calculates the maximum target current for PV mode
func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered, batteryStart bool) float64 {
func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryBoostPower float64, batteryBuffered, batteryStart bool) float64 {
// read only once to simplify testing
minCurrent := lp.effectiveMinCurrent()
maxCurrent := lp.effectiveMaxCurrent()

// push demand to drain battery
sitePower -= lp.boostPower(batteryBoostPower, batteryBuffered)

// switch phases up/down
var scaledTo int
if lp.hasPhaseSwitching() && lp.phaseSwitchCompleted() {
Expand Down Expand Up @@ -1656,7 +1705,7 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
}

// Update is the main control function. It reevaluates meters and charger state
func (lp *Loadpoint) Update(sitePower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
// smart cost
smartCostActive := lp.smartCostActive(rates)
lp.publish(keys.SmartCostActive, smartCostActive)
Expand Down Expand Up @@ -1781,7 +1830,7 @@ func (lp *Loadpoint) Update(sitePower float64, rates api.Rates, batteryBuffered,
break
}

targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBuffered, batteryStart)
targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBoostPower, batteryBuffered, batteryStart)

if targetCurrent == 0 && lp.vehicleClimateActive() {
targetCurrent = lp.effectiveMinCurrent()
Expand Down
5 changes: 5 additions & 0 deletions core/loadpoint/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ type API interface {
// SetDisableDelay sets loadpoint disable delay
SetDisableDelay(delay time.Duration)

// GetBatteryBoost returns the battery boost
GetBatteryBoost() bool
// SetBatteryBoost sets the battery boost
SetBatteryBoost(enable bool) error

// RemoteControl sets remote status demand
RemoteControl(string, RemoteDemand)

Expand Down
28 changes: 28 additions & 0 deletions core/loadpoint/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions core/loadpoint_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func (lp *Loadpoint) SetMode(mode api.ChargeMode) {
if lp.mode != mode {
lp.setMode(mode)

lp.batteryBoost = 0

// reset timers
switch mode {
case api.ModeNow, api.ModeOff:
Expand Down Expand Up @@ -316,6 +318,49 @@ func (lp *Loadpoint) SetDisableDelay(delay time.Duration) {
}
}

// getBatteryBoost returns the battery boost
func (lp *Loadpoint) getBatteryBoost() int {
lp.RLock()
defer lp.RUnlock()
return lp.batteryBoost
}

// GetBatteryBoost returns the battery boost
func (lp *Loadpoint) GetBatteryBoost() bool {
return lp.getBatteryBoost() > 0
}

// setBatteryBoost returns the battery boost
func (lp *Loadpoint) setBatteryBoost(boost int) {
lp.Lock()
defer lp.Unlock()
lp.batteryBoost = boost
}

// SetBatteryBoost sets the battery boost
func (lp *Loadpoint) SetBatteryBoost(enable bool) error {
lp.Lock()
defer lp.Unlock()

if enable && lp.mode != api.ModePV && lp.mode != api.ModeMinPV {
return errors.New("battery boost is only available in PV modes")
}

lp.log.DEBUG.Println("set battery boost:", enable)
andig marked this conversation as resolved.
Show resolved Hide resolved

if enable != (lp.batteryBoost != boostDisabled) {
lp.publish(keys.BatteryBoost, enable)

lp.batteryBoost = boostDisabled
if enable {
lp.batteryBoost = boostStart
lp.requestUpdate()
}
}

return nil
}

// RemoteControl sets remote status demand
func (lp *Loadpoint) RemoteControl(source string, demand loadpoint.RemoteDemand) {
lp.Lock()
Expand Down
5 changes: 5 additions & 0 deletions core/loadpoint_effective.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ func (lp *Loadpoint) effectiveLimitSoc() int {
return 100
}

// effectiveStepPower returns the effective step power for the currently active phases
func (lp *Loadpoint) effectiveStepPower() float64 {
return Voltage * float64(lp.ActivePhases())
}

// EffectiveMinPower returns the effective min power for the minimum active phases
func (lp *Loadpoint) EffectiveMinPower() float64 {
return Voltage * lp.effectiveMinCurrent() * float64(lp.minActivePhases())
Expand Down
34 changes: 17 additions & 17 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func TestUpdatePowerZero(t *testing.T) {
}

lp.mode = tc.mode
lp.Update(0, nil, false, false, 0, nil, nil) // false,sitePower false,0
lp.Update(0, 0, nil, false, false, 0, nil, nil) // false,sitePower false,0

ctrl.Finish()
}
Expand Down Expand Up @@ -338,7 +338,7 @@ func TestPVHysteresis(t *testing.T) {
// charger.EXPECT().Enabled().Return(tc.enabled, nil)

lp.enabled = tc.enabled
current := lp.pvMaxCurrent(api.ModePV, se.site, false, false)
current := lp.pvMaxCurrent(api.ModePV, se.site, 0, false, false)

if current != se.current {
t.Errorf("step %d: wanted %.1f, got %.1f", step, se.current, current)
Expand Down Expand Up @@ -371,7 +371,7 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {

// maxCurrent will read enabled state in PV mode
sitePower := -float64(phases)*minA*Voltage + 1 // 1W below min power
current := lp.pvMaxCurrent(api.ModePV, sitePower, false, false)
current := lp.pvMaxCurrent(api.ModePV, sitePower, 0, false, false)

if current != 0 {
t.Errorf("PV mode could not disable charger as expected. Expected 0, got %.f", current)
Expand Down Expand Up @@ -428,7 +428,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
lp.Update(500, nil, false, false, 0, nil, nil)
lp.Update(500, 0, nil, false, false, 0, nil, nil)
ctrl.Finish()

t.Log("charging above target - soc deactivates charger")
Expand All @@ -437,22 +437,22 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(false).Return(nil)
lp.Update(500, nil, false, false, 0, nil, nil)
lp.Update(500, 0, nil, false, false, 0, nil, nil)
ctrl.Finish()

t.Log("deactivated charger changes status to B")
clock.Add(5 * time.Minute)
vehicle.EXPECT().Soc().Return(95.0, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
lp.Update(-500, nil, false, false, 0, nil, nil)
lp.Update(-500, 0, nil, false, false, 0, nil, nil)
ctrl.Finish()

t.Log("soc has risen below target - soc update prevented by timer")
clock.Add(5 * time.Minute)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
lp.Update(-500, nil, false, false, 0, nil, nil)
lp.Update(-500, 0, nil, false, false, 0, nil, nil)
ctrl.Finish()

t.Log("soc has fallen below target - soc update timer expired")
Expand All @@ -462,7 +462,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
charger.EXPECT().Enable(true).Return(nil)
lp.Update(-500, nil, false, false, 0, nil, nil)
lp.Update(-500, 0, nil, false, false, 0, nil, nil)
ctrl.Finish()
}

Expand Down Expand Up @@ -497,14 +497,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
lp.Update(500, nil, false, false, 0, nil, nil)
lp.Update(500, 0, nil, false, false, 0, nil, nil)

t.Log("switch off when disconnected")
clock.Add(5 * time.Minute)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusA, nil)
charger.EXPECT().Enable(false).Return(nil)
lp.Update(-300, nil, false, false, 0, nil, nil)
lp.Update(-300, 0, nil, false, false, 0, nil, nil)

if mode := lp.GetMode(); mode != api.ModeOff {
t.Error("unexpected mode", mode)
Expand Down Expand Up @@ -567,46 +567,46 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(0.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, nil, false, false, 0, nil, nil)
lp.Update(-1, 0, nil, false, false, 0, nil, nil)

t.Log("at 1:00h charging at 5 kWh")
clock.Add(time.Hour)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, nil, false, false, 0, nil, nil)
lp.Update(-1, 0, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:00h stop charging at 5 kWh")
clock.Add(time.Second)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
lp.Update(-1, nil, false, false, 0, nil, nil)
lp.Update(-1, 0, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:00h restart charging at 5 kWh")
clock.Add(time.Second)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, nil, false, false, 0, nil, nil)
lp.Update(-1, 0, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:30h continue charging at 7.5 kWh")
clock.Add(30 * time.Minute)
rater.EXPECT().ChargedEnergy().Return(7.5, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, nil, false, false, 0, nil, nil)
lp.Update(-1, 0, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 7500.0)

t.Log("at 2:00h stop charging at 10 kWh")
clock.Add(30 * time.Minute)
rater.EXPECT().ChargedEnergy().Return(10.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
lp.Update(-1, nil, false, false, 0, nil, nil)
lp.Update(-1, 0, nil, false, false, 0, nil, nil)
expectCache("chargedEnergy", 10000.0)

ctrl.Finish()
Expand Down Expand Up @@ -758,7 +758,7 @@ func TestPVHysteresisAfterPhaseSwitch(t *testing.T) {

for step, se := range tc.series {
clck.Set(start.Add(se.delay))
assert.Equal(t, se.current, lp.pvMaxCurrent(api.ModePV, se.site, false, false), step)
assert.Equal(t, se.current, lp.pvMaxCurrent(api.ModePV, se.site, 0, false, false), step)
}

ctrl.Finish()
Expand Down
Loading