Add smartCostLimit to site (#6732)
This commit is contained in:
parent
1878897d1f
commit
f4f0d6bed0
11 changed files with 90 additions and 26 deletions
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -426,7 +426,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, false)
|
||||
lp.Update(500, false, false)
|
||||
|
||||
t.Log("charging above target - soc deactivates charger")
|
||||
clock.Add(5 * time.Minute)
|
||||
|
|
@ -434,20 +434,20 @@ 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, 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)
|
||||
|
|
@ -455,7 +455,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
|
|||
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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -564,14 +564,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, 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")
|
||||
|
|
@ -579,7 +579,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, false)
|
||||
lp.Update(-1, false, false)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:00h restart charging at 5 kWh")
|
||||
|
|
@ -587,7 +587,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, false)
|
||||
lp.Update(-1, false, false)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:30h continue charging at 7.5 kWh")
|
||||
|
|
@ -595,7 +595,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, false)
|
||||
lp.Update(-1, false, false)
|
||||
expectCache("chargedEnergy", 7500.0)
|
||||
|
||||
t.Log("at 2:00h stop charging at 10 kWh")
|
||||
|
|
@ -603,7 +603,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, false)
|
||||
lp.Update(-1, false, false)
|
||||
expectCache("chargedEnergy", 10000.0)
|
||||
|
||||
ctrl.Finish()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
23
core/site.go
23
core/site.go
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
17
schema.json
17
schema.json
|
|
@ -200,8 +200,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"prioritySoc": {},
|
||||
"bufferSoc": {}
|
||||
"residualPower": {
|
||||
"type": "number"
|
||||
},
|
||||
"prioritySoc": {
|
||||
"type": "number"
|
||||
},
|
||||
"bufferSoc": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxGridSupplyWhileBatteryCharging": {
|
||||
"type": "number"
|
||||
},
|
||||
"autoChargeCostLimit": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"loadpoints": {
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue