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 solar share setting (experimental) #16507

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions core/keys/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 27 additions & 5 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1352,16 +1356,26 @@ 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() {
// calculate site power after a phase switch from activePhases phases -> 1 phase
// 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())
Expand Down Expand Up @@ -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())
Expand Down
5 changes: 5 additions & 0 deletions core/loadpoint/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.

39 changes: 31 additions & 8 deletions core/loadpoint_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
4 changes: 4 additions & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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)},
}

Expand Down
24 changes: 24 additions & 0 deletions server/http_site_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions server/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions util/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down