diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go index a89b9a4a6..6f58fd953 100644 --- a/core/keys/loadpoint.go +++ b/core/keys/loadpoint.go @@ -12,6 +12,8 @@ const ( LimitEnergy = "limitEnergy" // limit energy EnableThreshold = "enableThreshold" DisableThreshold = "disableThreshold" + EnableDelay = "enableDelay" + DisableDelay = "disableDelay" 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 65ce37dd7..9dd38b802 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1194,9 +1194,9 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) in lp.phaseTimer = lp.clock.Now() } - lp.publishTimer(phaseTimer, lp.Disable.Delay, phaseScale1p) + lp.publishTimer(phaseTimer, lp.GetDisableDelay(), phaseScale1p) - if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.Disable.Delay { + if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.GetDisableDelay() { if err := lp.scalePhases(1); err != nil { lp.log.ERROR.Println(err) } @@ -1223,9 +1223,9 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) in lp.phaseTimer = lp.clock.Now() } - lp.publishTimer(phaseTimer, lp.Enable.Delay, phaseScale3p) + lp.publishTimer(phaseTimer, lp.GetEnableDelay(), phaseScale3p) - if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.Enable.Delay { + if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.GetEnableDelay() { if err := lp.scalePhases(3); err != nil { lp.log.ERROR.Println(err) } @@ -1307,21 +1307,21 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter lp.log.DEBUG.Printf("projected site power %.0fW >= %.0fW disable threshold", projectedSitePower, lp.Disable.Threshold) if lp.pvTimer.IsZero() { - lp.log.DEBUG.Printf("pv disable timer start: %v", lp.Disable.Delay) + lp.log.DEBUG.Printf("pv disable timer start: %v", lp.GetDisableDelay()) lp.pvTimer = lp.clock.Now() } - lp.publishTimer(pvTimer, lp.Disable.Delay, pvDisable) + lp.publishTimer(pvTimer, lp.GetDisableDelay(), pvDisable) elapsed := lp.clock.Since(lp.pvTimer) - if elapsed >= lp.Disable.Delay { + if elapsed >= lp.GetDisableDelay() { lp.log.DEBUG.Println("pv disable timer elapsed") return 0 } // suppress duplicate log message after timer started if elapsed > time.Second { - lp.log.DEBUG.Printf("pv disable timer remaining: %v", (lp.Disable.Delay - elapsed).Round(time.Second)) + lp.log.DEBUG.Printf("pv disable timer remaining: %v", (lp.GetDisableDelay() - elapsed).Round(time.Second)) } } else { // reset timer @@ -1339,21 +1339,21 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter lp.log.DEBUG.Printf("site power %.0fW <= %.0fW enable threshold", sitePower, lp.Enable.Threshold) if lp.pvTimer.IsZero() { - lp.log.DEBUG.Printf("pv enable timer start: %v", lp.Enable.Delay) + lp.log.DEBUG.Printf("pv enable timer start: %v", lp.GetEnableDelay()) lp.pvTimer = lp.clock.Now() } - lp.publishTimer(pvTimer, lp.Enable.Delay, pvEnable) + lp.publishTimer(pvTimer, lp.GetEnableDelay(), pvEnable) elapsed := lp.clock.Since(lp.pvTimer) - if elapsed >= lp.Enable.Delay { + if elapsed >= lp.GetEnableDelay() { lp.log.DEBUG.Println("pv enable timer elapsed") return minCurrent } // suppress duplicate log message after timer started if elapsed > time.Second { - lp.log.DEBUG.Printf("pv enable timer remaining: %v", (lp.Enable.Delay - elapsed).Round(time.Second)) + lp.log.DEBUG.Printf("pv enable timer remaining: %v", (lp.GetEnableDelay() - elapsed).Round(time.Second)) } } else { // reset timer diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index a6f4f3081..57f9498da 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -102,6 +102,15 @@ type API interface { // SetDisableThreshold sets loadpoint disable threshold SetDisableThreshold(threshold float64) + // GetEnableDelay gets the loadpoint enable delay + GetEnableDelay() time.Duration + // SetEnableDelay sets loadpoint enable delay + SetEnableDelay(delay time.Duration) + // GetDisableDelay gets the loadpoint disable delay + GetDisableDelay() time.Duration + // SetDisableDelay sets loadpoint disable delay + SetDisableDelay(delay time.Duration) + // RemoteControl sets remote status demand RemoteControl(string, RemoteDemand) diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index b1f4f3fea..4e1b2c6c8 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -152,6 +152,20 @@ func (mr *MockAPIMockRecorder) GetCircuit() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCircuit", reflect.TypeOf((*MockAPI)(nil).GetCircuit)) } +// GetDisableDelay mocks base method. +func (m *MockAPI) GetDisableDelay() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetDisableDelay") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// GetDisableDelay indicates an expected call of GetDisableDelay. +func (mr *MockAPIMockRecorder) GetDisableDelay() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDisableDelay", reflect.TypeOf((*MockAPI)(nil).GetDisableDelay)) +} + // GetDisableThreshold mocks base method. func (m *MockAPI) GetDisableThreshold() float64 { m.ctrl.T.Helper() @@ -166,6 +180,20 @@ func (mr *MockAPIMockRecorder) GetDisableThreshold() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDisableThreshold", reflect.TypeOf((*MockAPI)(nil).GetDisableThreshold)) } +// GetEnableDelay mocks base method. +func (m *MockAPI) GetEnableDelay() time.Duration { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEnableDelay") + ret0, _ := ret[0].(time.Duration) + return ret0 +} + +// GetEnableDelay indicates an expected call of GetEnableDelay. +func (mr *MockAPIMockRecorder) GetEnableDelay() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnableDelay", reflect.TypeOf((*MockAPI)(nil).GetEnableDelay)) +} + // GetEnableThreshold mocks base method. func (m *MockAPI) GetEnableThreshold() float64 { m.ctrl.T.Helper() @@ -473,6 +501,18 @@ func (mr *MockAPIMockRecorder) RemoteControl(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteControl", reflect.TypeOf((*MockAPI)(nil).RemoteControl), arg0, arg1) } +// SetDisableDelay mocks base method. +func (m *MockAPI) SetDisableDelay(arg0 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetDisableDelay", arg0) +} + +// SetDisableDelay indicates an expected call of SetDisableDelay. +func (mr *MockAPIMockRecorder) SetDisableDelay(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDisableDelay", reflect.TypeOf((*MockAPI)(nil).SetDisableDelay), arg0) +} + // SetDisableThreshold mocks base method. func (m *MockAPI) SetDisableThreshold(arg0 float64) { m.ctrl.T.Helper() @@ -485,6 +525,18 @@ func (mr *MockAPIMockRecorder) SetDisableThreshold(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDisableThreshold", reflect.TypeOf((*MockAPI)(nil).SetDisableThreshold), arg0) } +// SetEnableDelay mocks base method. +func (m *MockAPI) SetEnableDelay(arg0 time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetEnableDelay", arg0) +} + +// SetEnableDelay indicates an expected call of SetEnableDelay. +func (mr *MockAPIMockRecorder) SetEnableDelay(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEnableDelay", reflect.TypeOf((*MockAPI)(nil).SetEnableDelay), arg0) +} + // SetEnableThreshold mocks base method. func (m *MockAPI) SetEnableThreshold(arg0 float64) { m.ctrl.T.Helper() diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index b4b87eb0a..bc4082764 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -276,6 +276,46 @@ func (lp *Loadpoint) SetDisableThreshold(threshold float64) { } } +// GetEnableDelay gets the loadpoint enable delay +func (lp *Loadpoint) GetEnableDelay() time.Duration { + lp.RLock() + defer lp.RUnlock() + return lp.Enable.Delay +} + +// SetEnableDelay sets loadpoint enable delay +func (lp *Loadpoint) SetEnableDelay(delay time.Duration) { + lp.Lock() + defer lp.Unlock() + + lp.log.DEBUG.Println("set enable delay:", delay) + + if lp.Enable.Delay != delay { + lp.Enable.Delay = delay + lp.publish(keys.EnableDelay, delay) + } +} + +// GetDisableDelay gets the loadpoint enable delay +func (lp *Loadpoint) GetDisableDelay() time.Duration { + lp.RLock() + defer lp.RUnlock() + return lp.Disable.Delay +} + +// SetDisableDelay sets loadpoint disable delay +func (lp *Loadpoint) SetDisableDelay(delay time.Duration) { + lp.Lock() + defer lp.Unlock() + + lp.log.DEBUG.Println("set disable delay:", delay) + + if lp.Disable.Delay != delay { + lp.Disable.Delay = delay + lp.publish(keys.DisableDelay, delay) + } +} + // RemoteControl sets remote status demand func (lp *Loadpoint) RemoteControl(source string, demand loadpoint.RemoteDemand) { lp.Lock() diff --git a/server/http.go b/server/http.go index e394241eb..7d433c3cc 100644 --- a/server/http.go +++ b/server/http.go @@ -175,7 +175,9 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param) "vehicleDetect": {"PATCH", "/vehicle", vehicleDetectHandler(lp)}, "remotedemand": {"POST", "/remotedemand/{demand:[a-z]+}/{source:[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)}, "enableThreshold": {"POST", "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)}, + "enableDelay": {"POST", "/enable/delay/{value:[0-9]+}", durationHandler(pass(lp.SetEnableDelay), lp.GetEnableDelay)}, "disableThreshold": {"POST", "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)}, + "disableDelay": {"POST", "/disable/delay/{value:[0-9]+}", durationHandler(pass(lp.SetDisableDelay), lp.GetDisableDelay)}, "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.]+}", floatHandler(pass(lp.SetPriority), lp.GetPriority)}, diff --git a/server/http_site_handler.go b/server/http_site_handler.go index 622af418c..c04ce38fc 100644 --- a/server/http_site_handler.go +++ b/server/http_site_handler.go @@ -162,6 +162,11 @@ func boolGetHandler(get func() bool) http.HandlerFunc { } } +// durationHandler updates duration-param api +func durationHandler(set func(time.Duration) error, get func() time.Duration) http.HandlerFunc { + return handler(util.ParseDuration, set, get) +} + // updateSmartCostLimit sets the smart cost limit globally func updateSmartCostLimit(site site.API) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/server/mqtt.go b/server/mqtt.go index c44a4043c..cc4b0dc61 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -203,6 +203,8 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint. {"/limitEnergy", floatSetter(pass(lp.SetLimitEnergy))}, {"/enableThreshold", floatSetter(pass(lp.SetEnableThreshold))}, {"/disableThreshold", floatSetter(pass(lp.SetDisableThreshold))}, + {"/enableDelay", durationSetter(pass(lp.SetEnableDelay))}, + {"/disableDelay", durationSetter(pass(lp.SetDisableDelay))}, {"/smartCostLimit", floatPtrSetter(pass(lp.SetSmartCostLimit))}, {"/planEnergy", func(payload string) error { var plan struct { diff --git a/server/mqtt_setter.go b/server/mqtt_setter.go index e45cac70d..ee6214d21 100644 --- a/server/mqtt_setter.go +++ b/server/mqtt_setter.go @@ -2,7 +2,9 @@ package server import ( "strconv" + "time" + "github.com/evcc-io/evcc/util" "github.com/spf13/cast" ) @@ -46,3 +48,7 @@ func boolSetter(set func(bool) error) func(string) error { return cast.ToBoolE(v) }, set) } + +func durationSetter(set func(time.Duration) error) func(string) error { + return setterFunc(util.ParseDuration, set) +} diff --git a/util/duration.go b/util/duration.go new file mode 100644 index 000000000..32fde38f3 --- /dev/null +++ b/util/duration.go @@ -0,0 +1,15 @@ +package util + +import ( + "strconv" + "time" +) + +func ParseDuration(s string) (time.Duration, error) { + v, err := strconv.Atoi(s) + if err != nil { + return 0, err + } + + return time.Duration(v) * time.Second, nil +}