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 2 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
12 changes: 9 additions & 3 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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 bool // battery boost mode

mode api.ChargeMode
enabled bool // Charger enabled state
Expand Down Expand Up @@ -1272,11 +1273,16 @@ func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action strin
}

// 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 float64, batteryBuffered, batteryStart, batteryBoost bool) float64 {
// read only once to simplify testing
minCurrent := lp.effectiveMinCurrent()
maxCurrent := lp.effectiveMaxCurrent()

// TODO depends on https://github.com/evcc-io/evcc/pull/16274
if minPower := lp.EffectiveMinPower(); lp.batteryBoost && batteryBoost {
sitePower -= minPower
}

// switch phases up/down
var scaledTo int
if lp.hasPhaseSwitching() && lp.phaseSwitchCompleted() {
Expand Down Expand Up @@ -1654,7 +1660,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 float64, rates api.Rates, batteryBuffered, batteryStart, batteryBoost bool, greenShare float64, effPrice, effCo2 *float64) {
// smart cost
smartCostActive := lp.smartCostActive(rates)
lp.publish(keys.SmartCostActive, smartCostActive)
Expand Down Expand Up @@ -1779,7 +1785,7 @@ func (lp *Loadpoint) Update(sitePower float64, rates api.Rates, batteryBuffered,
break
}

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

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)

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

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

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

23 changes: 23 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 = false

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

// GetBatteryBoost returns the battery boost
func (lp *Loadpoint) GetBatteryBoost() bool {
lp.Lock()
defer lp.Unlock()

return lp.batteryBoost
}

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

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

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

// RemoteControl sets remote status demand
func (lp *Loadpoint) RemoteControl(source string, demand loadpoint.RemoteDemand) {
lp.Lock()
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, nil, false, 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, false, 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, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, nil, false, 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, false, false, false), step)
}

ctrl.Finish()
Expand Down
4 changes: 2 additions & 2 deletions core/loadpoint_vehicle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func TestReconnectVehicle(t *testing.T) {
// vehicle not updated yet
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)

lp.Update(0, nil, false, false, 0, nil, nil)
lp.Update(0, nil, false, false, false, 0, nil, nil)
ctrl.Finish()

// detection started
Expand All @@ -284,7 +284,7 @@ func TestReconnectVehicle(t *testing.T) {
// vehicle not updated yet
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil)

lp.Update(0, nil, false, false, 0, nil, nil)
lp.Update(0, nil, false, false, false, 0, nil, nil)
ctrl.Finish()

// vehicle detected
Expand Down
8 changes: 6 additions & 2 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const standbyPower = 10 // consider less than 10W as charger in standby
// updater abstracts the Loadpoint implementation for testing
type updater interface {
loadpoint.API
Update(availablePower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
Update(sitePower float64, rates api.Rates, batteryBuffered, batteryStart, batteryBoost bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
}

// meterMeasurement is used as slice element for publishing structured data
Expand Down Expand Up @@ -942,7 +942,11 @@ func (site *Site) update(lp updater) {
greenShareHome := site.greenShare(0, homePower)
greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)

lp.Update(sitePower, rates, batteryBuffered, batteryStart, greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints))
batteryBoost := site.gridPower < 100 // W
lp.Update(
sitePower, rates, batteryBuffered, batteryStart, batteryBoost,
greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints),
)

site.Health.Update()

Expand Down
1 change: 1 addition & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
"smartCost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", floatPtrHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
"smartCostDelete": {"DELETE", "/smartcostlimit", floatPtrHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
"priority": {"POST", "/priority/{value:[0-9]+}", intHandler(pass(lp.SetPriority), lp.GetPriority)},
"boost": {"POST", "/boost/{value:[0-1]}", boolHandler(pass(lp.SetBatteryBoost), lp.GetBatteryBoost)},
}

for _, r := range routes {
Expand Down