diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go index 6f58fd953..4b973da8a 100644 --- a/core/keys/loadpoint.go +++ b/core/keys/loadpoint.go @@ -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) diff --git a/core/loadpoint.go b/core/loadpoint.go index 77205f366..29eae0239 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -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 @@ -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 @@ -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) @@ -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 @@ -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() { @@ -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) @@ -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() diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index ce12b4781..dc13a4be7 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -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) diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index 4e1b2c6c8..3f5e9b2b7 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -110,6 +110,20 @@ func (mr *MockAPIMockRecorder) EffectivePriority() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EffectivePriority", reflect.TypeOf((*MockAPI)(nil).EffectivePriority)) } +// GetBatteryBoost mocks base method. +func (m *MockAPI) GetBatteryBoost() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBatteryBoost") + ret0, _ := ret[0].(bool) + return ret0 +} + +// GetBatteryBoost indicates an expected call of GetBatteryBoost. +func (mr *MockAPIMockRecorder) GetBatteryBoost() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBatteryBoost", reflect.TypeOf((*MockAPI)(nil).GetBatteryBoost)) +} + // GetChargePower mocks base method. func (m *MockAPI) GetChargePower() float64 { m.ctrl.T.Helper() @@ -501,6 +515,20 @@ func (mr *MockAPIMockRecorder) RemoteControl(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteControl", reflect.TypeOf((*MockAPI)(nil).RemoteControl), arg0, arg1) } +// SetBatteryBoost mocks base method. +func (m *MockAPI) SetBatteryBoost(arg0 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetBatteryBoost", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetBatteryBoost indicates an expected call of SetBatteryBoost. +func (mr *MockAPIMockRecorder) SetBatteryBoost(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryBoost", reflect.TypeOf((*MockAPI)(nil).SetBatteryBoost), arg0) +} + // SetDisableDelay mocks base method. func (m *MockAPI) SetDisableDelay(arg0 time.Duration) { m.ctrl.T.Helper() diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index bc4082764..8ea427821 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -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: @@ -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) + + 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() diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go index 6d5ffa80b..d7448e66a 100644 --- a/core/loadpoint_effective.go +++ b/core/loadpoint_effective.go @@ -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()) diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 26fdeef98..4adfbb80e 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -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() } @@ -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) @@ -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) @@ -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") @@ -437,7 +437,7 @@ 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") @@ -445,14 +445,14 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) { 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") @@ -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() } @@ -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) @@ -567,14 +567,14 @@ 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") @@ -582,7 +582,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -590,7 +590,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -598,7 +598,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -606,7 +606,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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() @@ -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() diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index 5b8467962..d60cf8da7 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -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, 0, nil, false, false, 0, nil, nil) ctrl.Finish() // detection started @@ -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, 0, nil, false, false, 0, nil, nil) ctrl.Finish() // vehicle detected diff --git a/core/site.go b/core/site.go index cd627ea5c..b211438a4 100644 --- a/core/site.go +++ b/core/site.go @@ -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, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64) } // meterMeasurement is used as slice element for publishing structured data @@ -942,7 +942,10 @@ 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)) + lp.Update( + sitePower, max(0, site.batteryPower), rates, batteryBuffered, batteryStart, + greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints), + ) site.Health.Update() diff --git a/server/http.go b/server/http.go index 05bd273fc..d43e68c64 100644 --- a/server/http.go +++ b/server/http.go @@ -104,7 +104,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param) "health": {"GET", "/health", healthHandler(site)}, "buffersoc": {"POST", "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)}, "bufferstartsoc": {"POST", "/bufferstartsoc/{value:[0-9.]+}", floatHandler(site.SetBufferStartSoc, site.GetBufferStartSoc)}, - "batterydischargecontrol": {"POST", "/batterydischargecontrol/{value:[a-z]+}", boolHandler(site.SetBatteryDischargeControl, site.GetBatteryDischargeControl)}, + "batterydischargecontrol": {"POST", "/batterydischargecontrol/{value:[01truefalse]+}", boolHandler(site.SetBatteryDischargeControl, site.GetBatteryDischargeControl)}, "batterygridcharge": {"POST", "/batterygridchargelimit/{value:-?[0-9.]+}", floatPtrHandler(pass(site.SetBatteryGridChargeLimit), site.GetBatteryGridChargeLimit)}, "batterygridchargedelete": {"DELETE", "/batterygridchargelimit", floatPtrHandler(pass(site.SetBatteryGridChargeLimit), site.GetBatteryGridChargeLimit)}, "maxgridsupply": {"POST", "/maxgridsupply/{value:[0-9.]+}", floatHandler(site.SetMaxGridSupplyWhileBatteryCharging, site.GetMaxGridSupplyWhileBatteryCharging)}, @@ -117,7 +117,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param) "updatesession": {"PUT", "/session/{id:[0-9]+}", updateSessionHandler}, "deletesession": {"DELETE", "/session/{id:[0-9]+}", deleteSessionHandler}, "telemetry": {"GET", "/settings/telemetry", boolGetHandler(telemetry.Enabled)}, - "telemetry2": {"POST", "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)}, + "telemetry2": {"POST", "/settings/telemetry/{value:[01truefalse]+}", boolHandler(telemetry.Enable, telemetry.Enabled)}, } for _, r := range routes { @@ -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)}, + "batteryBoost": {"POST", "/batteryboost/{value:[01truefalse]}", boolHandler(lp.SetBatteryBoost, lp.GetBatteryBoost)}, } for _, r := range routes { diff --git a/server/mqtt.go b/server/mqtt.go index cc4b0dc61..a676c71b9 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -206,6 +206,7 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint. {"/enableDelay", durationSetter(pass(lp.SetEnableDelay))}, {"/disableDelay", durationSetter(pass(lp.SetDisableDelay))}, {"/smartCostLimit", floatPtrSetter(pass(lp.SetSmartCostLimit))}, + {"/batteryBoost", boolSetter(lp.SetBatteryBoost)}, {"/planEnergy", func(payload string) error { var plan struct { Time time.Time `json:"time"`