diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go index 4b973da8a9..bf803173af 100644 --- a/core/keys/loadpoint.go +++ b/core/keys/loadpoint.go @@ -35,6 +35,7 @@ const ( SmartCostActive = "smartCostActive" // smart cost active SmartCostLimit = "smartCostLimit" // smart cost limit SmartCostNextStart = "smartCostNextStart" // smart cost next start + SolarShare = "solarShare" // solar share // effective values EffectivePriority = "effectivePriority" // effective priority diff --git a/core/loadpoint.go b/core/loadpoint.go index 29eae02391..9d33ce7727 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -132,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 + solarShare *float64 // solar share for pv mode batteryBoost int // battery boost state mode api.ChargeMode @@ -345,6 +346,9 @@ func (lp *Loadpoint) restoreSettings() { if v, err := lp.settings.Float(keys.SmartCostLimit); err == nil { lp.SetSmartCostLimit(&v) } + if v, err := lp.settings.Float(keys.SolarShare); err == nil { + lp.SetSolarShare(&v) + } t, err1 := lp.settings.Time(keys.PlanTime) v, err2 := lp.settings.Float(keys.PlanEnergy) @@ -1352,6 +1356,9 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryBoostPo lp.log.DEBUG.Printf("pv charge current: %.3gA = %.3gA + %.3gA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, activePhases) + // inactive if nil + solarShare := lp.GetSolarShare() + if mode == api.ModePV && lp.enabled && targetCurrent < minCurrent { projectedSitePower := sitePower if !lp.phaseTimer.IsZero() { @@ -1359,9 +1366,16 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryBoostPo // notes: activePhases can be 1, 2 or 3 and phaseTimer can only be active if lp current is already at minCurrent projectedSitePower -= Voltage * minCurrent * float64(activePhases-1) } + + // lp.Disable.Threshold + disableThreshold := lp.Disable.Threshold + if solarShare != nil { + disableThreshold = (*solarShare - 1) * lp.EffectiveMinPower() + } + // kick off disable sequence - if projectedSitePower >= lp.Disable.Threshold { - lp.log.DEBUG.Printf("projected site power %.0fW >= %.0fW disable threshold", projectedSitePower, lp.Disable.Threshold) + if projectedSitePower >= disableThreshold { + lp.log.DEBUG.Printf("projected site power %.0fW >= %.0fW disable threshold", projectedSitePower, disableThreshold) if lp.pvTimer.IsZero() { lp.log.DEBUG.Printf("pv disable timer start: %v", lp.GetDisableDelay()) @@ -1390,10 +1404,18 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryBoostPo } if mode == api.ModePV && !lp.enabled { + // lp.Enable.Threshold + enableThreshold := lp.Enable.Threshold + shouldEnable := (lp.Enable.Threshold == 0 && targetCurrent >= minCurrent) || (lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) + + if solarShare != nil { + enableThreshold = -*solarShare * lp.EffectiveMinPower() + shouldEnable = sitePower <= enableThreshold + } + // kick off enable sequence - if (lp.Enable.Threshold == 0 && targetCurrent >= minCurrent) || - (lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) { - lp.log.DEBUG.Printf("site power %.0fW <= %.0fW enable threshold", sitePower, lp.Enable.Threshold) + if shouldEnable { + lp.log.DEBUG.Printf("site power %.0fW <= %.0fW enable threshold", sitePower, enableThreshold) if lp.pvTimer.IsZero() { lp.log.DEBUG.Printf("pv enable timer start: %v", lp.GetEnableDelay()) diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go index dc13a4be75..ffbae67a7b 100644 --- a/core/loadpoint/api.go +++ b/core/loadpoint/api.go @@ -102,6 +102,11 @@ type API interface { // SetDisableThreshold sets loadpoint disable threshold SetDisableThreshold(threshold float64) + // GetSolarShare gets the solar share + GetSolarShare() *float64 + // SetSolarShare sets the solar share + SetSolarShare(*float64) + // GetEnableDelay gets the loadpoint enable delay GetEnableDelay() time.Duration // SetEnableDelay sets loadpoint enable delay diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go index 3f5e9b2b75..15a6abe84b 100644 --- a/core/loadpoint/mock.go +++ b/core/loadpoint/mock.go @@ -435,6 +435,20 @@ func (mr *MockAPIMockRecorder) GetSmartCostLimit() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).GetSmartCostLimit)) } +// GetSolarShare mocks base method. +func (m *MockAPI) GetSolarShare() *float64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSolarShare") + ret0, _ := ret[0].(*float64) + return ret0 +} + +// GetSolarShare indicates an expected call of GetSolarShare. +func (mr *MockAPIMockRecorder) GetSolarShare() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSolarShare", reflect.TypeOf((*MockAPI)(nil).GetSolarShare)) +} + // GetStatus mocks base method. func (m *MockAPI) GetStatus() api.ChargeStatus { m.ctrl.T.Helper() @@ -693,6 +707,18 @@ func (mr *MockAPIMockRecorder) SetSmartCostLimit(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).SetSmartCostLimit), arg0) } +// SetSolarShare mocks base method. +func (m *MockAPI) SetSolarShare(arg0 *float64) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetSolarShare", arg0) +} + +// SetSolarShare indicates an expected call of SetSolarShare. +func (mr *MockAPIMockRecorder) SetSolarShare(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSolarShare", reflect.TypeOf((*MockAPI)(nil).SetSolarShare), arg0) +} + // SetVehicle mocks base method. func (m *MockAPI) SetVehicle(arg0 api.Vehicle) { m.ctrl.T.Helper() diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index 8ea4278215..858b8c3f20 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -580,23 +580,46 @@ func (lp *Loadpoint) GetSmartCostLimit() *float64 { return lp.smartCostLimit } +func (lp *Loadpoint) savePointerValue(key string, val *float64) { + if val == nil { + lp.settings.SetString(key, "") + lp.publish(key, nil) + } else { + lp.settings.SetFloat(key, *val) + lp.publish(key, *val) + } +} + // SetSmartCostLimit sets the smart cost limit func (lp *Loadpoint) SetSmartCostLimit(val *float64) { lp.Lock() defer lp.Unlock() - lp.log.DEBUG.Println("set smart cost limit:", printPtr("%.1f", val)) + lp.log.DEBUG.Println("set smart cost limit:", printPtr("%.2f", val)) if !ptrValueEqual(lp.smartCostLimit, val) { lp.smartCostLimit = val + lp.savePointerValue(keys.SmartCostLimit, val) + } +} - if val == nil { - lp.settings.SetString(keys.SmartCostLimit, "") - lp.publish(keys.SmartCostLimit, nil) - } else { - lp.settings.SetFloat(keys.SmartCostLimit, *val) - lp.publish(keys.SmartCostLimit, *val) - } +// GetSolarShare gets the solar share +func (lp *Loadpoint) GetSolarShare() *float64 { + lp.RLock() + defer lp.RUnlock() + return lp.solarShare +} + +// SetSolarShare sets the solar share +func (lp *Loadpoint) SetSolarShare(val *float64) { + lp.Lock() + defer lp.Unlock() + + lp.log.DEBUG.Println("set solar share:", printPtr("%.2f", val)) + + if !ptrValueEqual(lp.solarShare, val) { + lp.solarShare = val + lp.savePointerValue(keys.SolarShare, val) } } diff --git a/server/http.go b/server/http.go index d43e68c641..8cfb55c6dc 100644 --- a/server/http.go +++ b/server/http.go @@ -112,6 +112,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param) "residualpower": {"POST", "/residualpower/{value:-?[0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)}, "smartcost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", updateSmartCostLimit(site)}, "smartcostdelete": {"DELETE", "/smartcostlimit", updateSmartCostLimit(site)}, + "solarshare": {"POST", "/solarshare/{value:[-0-9.]+}", updateSolarShare(site)}, + "solarshareDelete": {"DELETE", "/solarshare", updateSolarShare(site)}, "tariff": {"GET", "/tariff/{tariff:[a-z]+}", tariffHandler(site)}, "sessions": {"GET", "/sessions", sessionHandler}, "updatesession": {"PUT", "/session/{id:[0-9]+}", updateSessionHandler}, @@ -181,6 +183,8 @@ 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)}, + "solarshare": {"POST", "/solarshare/{value:-?[0-9.]+}", floatPtrHandler(pass(lp.SetSolarShare), lp.GetSolarShare)}, + "solarshareDelete": {"DELETE", "/solarshare", floatPtrHandler(pass(lp.SetSolarShare), lp.GetSolarShare)}, "batteryBoost": {"POST", "/batteryboost/{value:[01truefalse]}", boolHandler(lp.SetBatteryBoost, lp.GetBatteryBoost)}, } diff --git a/server/http_site_handler.go b/server/http_site_handler.go index c04ce38fc5..46bfd04799 100644 --- a/server/http_site_handler.go +++ b/server/http_site_handler.go @@ -191,6 +191,30 @@ func updateSmartCostLimit(site site.API) http.HandlerFunc { } } +// updateSolarShare sets the solar share limit globally +func updateSolarShare(site site.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + var val *float64 + + if r.Method != http.MethodDelete { + f, err := parseFloat(vars["value"]) + if err != nil { + jsonError(w, http.StatusBadRequest, err) + return + } + + val = &f + } + + for _, lp := range site.Loadpoints() { + lp.SetSolarShare(val) + } + + jsonResult(w, val) + } +} + // stateHandler returns the combined state func stateHandler(cache *util.Cache) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { diff --git a/server/mqtt.go b/server/mqtt.go index a676c71b94..8448076b10 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))}, + {"/solarShare", floatPtrSetter(pass(lp.SetSolarShare))}, {"/batteryBoost", boolSetter(lp.SetBatteryBoost)}, {"/planEnergy", func(payload string) error { var plan struct { diff --git a/util/duration.go b/util/duration.go index 32fde38f38..ed315eb943 100644 --- a/util/duration.go +++ b/util/duration.go @@ -5,6 +5,7 @@ import ( "time" ) +// ParseDuration parses a string as integer seconds value and returns a time.Duration func ParseDuration(s string) (time.Duration, error) { v, err := strconv.Atoi(s) if err != nil {