Skip to content

Commit

Permalink
Add smartCostLimit to site (evcc-io#6732)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Mar 19, 2023
1 parent 1878897 commit f4f0d6b
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 26 deletions.
10 changes: 9 additions & 1 deletion core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1441,7 +1441,7 @@ func (lp *Loadpoint) processTasks() {
}

// Update is the main control function. It reevaluates meters and charger state
func (lp *Loadpoint) Update(sitePower float64, batteryBuffered bool) {
func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered bool) {
lp.processTasks()

mode := lp.GetMode()
Expand Down Expand Up @@ -1531,6 +1531,14 @@ func (lp *Loadpoint) Update(sitePower float64, batteryBuffered bool) {
lp.elapsePVTimer() // let PV mode disable immediately afterwards

case mode == api.ModeMinPV || mode == api.ModePV:
// cheap tariff
if autoCharge && lp.GetTargetTime().IsZero() {
err = lp.fastCharging()
lp.resetPhaseTimer()
lp.elapsePVTimer() // let PV mode disable immediately afterwards
break
}

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

var required bool // false
Expand Down
4 changes: 2 additions & 2 deletions core/loadpoint_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (lp *Loadpoint) GetPlan(targetTime time.Time, maxPower float64) (time.Durat
return requiredDuration, plan, err
}

// plannerActive checks if charging plan is active
// plannerActive checks if the charging plan has an active slot
func (lp *Loadpoint) plannerActive() (active bool) {
defer func() {
lp.setPlanActive(active)
Expand All @@ -110,7 +110,7 @@ func (lp *Loadpoint) plannerActive() (active bool) {
requiredDuration.Round(time.Second), lp.targetTime.Round(time.Second).Local(), maxPower,
planner.Duration(plan).Round(time.Second), planner.AverageCost(plan))

// sort plan by time
// log plan
for _, slot := range plan {
lp.log.TRACE.Printf(" slot from: %v to %v cost %.3f", slot.Start.Round(time.Second).Local(), slot.End.Round(time.Second).Local(), slot.Price)
}
Expand Down
28 changes: 14 additions & 14 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func TestUpdatePowerZero(t *testing.T) {
}

lp.Mode = tc.mode
lp.Update(0, false) // sitePower 0
lp.Update(0, false, false) // sitePower false,0

ctrl.Finish()
}
Expand Down Expand Up @@ -426,36 +426,36 @@ 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, false)
lp.Update(500, false, false)

t.Log("charging above target - soc deactivates charger")
clock.Add(5 * time.Minute)
vehicle.EXPECT().Soc().Return(90.0, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(false).Return(nil)
lp.Update(500, false)
lp.Update(500, false, false)

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(-5000, false)
lp.Update(-5000, false, false)

t.Log("soc has fallen 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(-5000, false)
lp.Update(-5000, false, false)

t.Log("soc has fallen below target - soc update timer expired")
clock.Add(pollInterval)
vehicle.EXPECT().Soc().Return(85.0, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(true).Return(nil)
lp.Update(-5000, false)
lp.Update(-5000, false, false)

ctrl.Finish()
}
Expand Down Expand Up @@ -495,14 +495,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, false)
lp.Update(500, false, false)

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(-3000, false)
lp.Update(-3000, false, false)

if lp.Mode != api.ModeOff {
t.Error("unexpected mode", lp.Mode)
Expand Down Expand Up @@ -564,46 +564,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, false)
lp.Update(-1, false, false)

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, false)
lp.Update(-1, false, false)
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, false)
lp.Update(-1, false, false)
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, false)
lp.Update(-1, false, false)
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, false)
lp.Update(-1, false, false)
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, false)
lp.Update(-1, false, false)
expectCache("chargedEnergy", 10000.0)

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 @@ -359,7 +359,7 @@ func TestReconnectVehicle(t *testing.T) {
// vehicle not updated yet
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)

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

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

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

// vehicle detected
Expand Down
23 changes: 21 additions & 2 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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, batteryBuffered bool)
Update(availablePower float64, autoCharge, batteryBuffered bool)
}

// meterMeasurement is used as slice element for publishing structured data
Expand Down Expand Up @@ -60,6 +60,7 @@ type Site struct {
PrioritySoc float64 `mapstructure:"prioritySoc"` // prefer battery up to this Soc
BufferSoc float64 `mapstructure:"bufferSoc"` // ignore battery above this Soc
MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value
SmartCostLimit float64 `mapstructure:"smartCostLimit"` // always charge if cost is below this value

// meters
gridMeter api.Meter // Grid usage meter
Expand Down Expand Up @@ -633,8 +634,25 @@ func (site *Site) update(lp Updater) {
flexiblePower = site.prioritizer.GetChargePowerFlexibility(lp)
}

var autoCharge bool
if tariff := site.GetTariff(PlannerTariff); tariff != nil {
rates, err := tariff.Rates()

var rate api.Rate
if err == nil {
rate, err = rates.Current(time.Now())
}

if err == nil {
limit := site.GetSmartCostLimit()
autoCharge = limit != 0 && rate.Price <= limit
} else {
site.log.ERROR.Println("tariff:", err)
}
}

if sitePower, batteryBuffered, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
lp.Update(sitePower, batteryBuffered)
lp.Update(sitePower, autoCharge, batteryBuffered)

// ignore negative pvPower values as that means it is not an energy source but consumption
homePower := site.gridPower + math.Max(0, site.pvPower) + site.batteryPower - totalChargePower
Expand Down Expand Up @@ -664,6 +682,7 @@ func (site *Site) prepare() {
site.publish("bufferSoc", site.BufferSoc)
site.publish("prioritySoc", site.PrioritySoc)
site.publish("residualPower", site.ResidualPower)
site.publish("SmartCostLimit", site.SmartCostLimit)

site.publish("currency", site.tariffs.Currency.String())
site.publish("savingsSince", site.savings.Since())
Expand Down
4 changes: 3 additions & 1 deletion core/site/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ type API interface {
GetVehicles() []api.Vehicle

//
// Tariffs
// tariffs and costs
//

// GetTariff returns the respective tariff
GetTariff(string) api.Tariff
GetSmartCostLimit() float64
SetSmartCostLimit(float64) error
}
18 changes: 18 additions & 0 deletions core/site_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ func (site *Site) SetResidualPower(power float64) error {
return nil
}

// GetSmartCostLimit returns the SmartCostLimit
func (site *Site) GetSmartCostLimit() float64 {
site.Lock()
defer site.Unlock()
return site.SmartCostLimit
}

// SetSmartCostLimit sets the SmartCostLimit
func (site *Site) SetSmartCostLimit(val float64) error {
site.Lock()
defer site.Unlock()

site.SmartCostLimit = val
site.publish("smartCostLimit", site.SmartCostLimit)

return nil
}

// GetVehicles is the list of vehicles
func (site *Site) GetVehicles() []api.Vehicle {
site.Lock()
Expand Down
3 changes: 3 additions & 0 deletions evcc.dist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,11 @@ site:
battery:
- battery # list of battery meters
aux: aux # auxiliary meters for adjusting grid operating point
residualPower: # additional household usage margin
prioritySoc: # give home battery priority up to this soc (empty to disable)
bufferSoc: # ignore home battery discharge above soc (empty to disable)
maxGridSupplyWhileBatteryCharging: # ignore battery charging if AC consumption is above this value
smartCostLimit: # set cost limit for automatic charging in PV mode

# loadpoint describes the charger, charge meter and connected vehicle
loadpoints:
Expand Down
4 changes: 2 additions & 2 deletions hems/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"strings"

"github.com/evcc-io/evcc/core"
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/hems/ocpp"
"github.com/evcc-io/evcc/hems/semp"
"github.com/evcc-io/evcc/server"
Expand All @@ -16,7 +16,7 @@ type HEMS interface {
}

// NewFromConfig creates new HEMS from config
func NewFromConfig(typ string, other map[string]interface{}, site *core.Site, httpd *server.HTTPd) (HEMS, error) {
func NewFromConfig(typ string, other map[string]interface{}, site site.API, httpd *server.HTTPd) (HEMS, error) {
switch strings.ToLower(typ) {
case "sma", "shm", "semp":
return semp.New(other, site, httpd)
Expand Down
17 changes: 15 additions & 2 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,21 @@
}
}
},
"prioritySoc": {},
"bufferSoc": {}
"residualPower": {
"type": "number"
},
"prioritySoc": {
"type": "number"
},
"bufferSoc": {
"type": "number"
},
"maxGridSupplyWhileBatteryCharging": {
"type": "number"
},
"autoChargeCostLimit": {
"type": "number"
}
}
},
"loadpoints": {
Expand Down
1 change: 1 addition & 0 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
"buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)},
"prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)},
"residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
"smartcost": {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[-0-9.]+}", floatHandler(site.SetSmartCostLimit, site.GetSmartCostLimit)},
"tariff": {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
"sessions": {[]string{"GET"}, "/sessions", sessionHandler},
"session1": {[]string{"PUT"}, "/session/{id:[0-9]+}", updateSessionHandler},
Expand Down

0 comments on commit f4f0d6b

Please sign in to comment.