Add smart feed-in priority (#21813)
This commit is contained in:
parent
6ba804b106
commit
42ed7da2e9
63 changed files with 1645 additions and 1290 deletions
|
|
@ -42,8 +42,12 @@ const (
|
|||
|
||||
// smart charging
|
||||
SmartCostActive = "smartCostActive" // smart cost active
|
||||
SmartCostLimit = "smartCostLimit" // smart cost limit
|
||||
SmartCostNextStart = "smartCostNextStart" // smart cost next start
|
||||
SmartCostLimit = "smartCostLimit" // smart cost limit, fast charge when costs are below
|
||||
SmartCostNextStart = "smartCostNextStart" // smart cost next start, time of next fast charging
|
||||
|
||||
SmartFeedInPriorityActive = "smartFeedInPriorityActive" // smart feed-in priority active
|
||||
SmartFeedInPriorityLimit = "smartFeedInPriorityLimit" // smart feed-in priority limit, pause self-consumption when feed-in rates are above
|
||||
SmartFeedInPriorityNextStart = "smartFeedInPriorityNextStart" // smart feed-in priority next start, time of next pause
|
||||
|
||||
// effective values
|
||||
EffectivePriority = "effectivePriority" // effective priority
|
||||
|
|
|
|||
|
|
@ -56,4 +56,8 @@ const (
|
|||
|
||||
// external battery control
|
||||
BatteryModeExternal = "batteryModeExternal"
|
||||
|
||||
// smart charging
|
||||
SmartCostAvailable = "smartCostAvailable" // smart cost available
|
||||
SmartFeedInPriorityAvailable = "smartFeedInPriorityAvailable" // smart feed-in priority available
|
||||
)
|
||||
|
|
|
|||
|
|
@ -107,15 +107,16 @@ type Loadpoint struct {
|
|||
MinCurrent_ float64 `mapstructure:"minCurrent"` // ignored, present for compatibility
|
||||
MaxCurrent_ float64 `mapstructure:"maxCurrent"` // ignored, present for compatibility
|
||||
|
||||
title string // UI title
|
||||
priority int // Priority
|
||||
minCurrent float64 // PV mode: start current Min+PV mode: min current
|
||||
maxCurrent float64 // Max allowed current. Physically ensured by the charger
|
||||
phasesConfigured int // Charger configured phase mode 0/1/3
|
||||
limitSoc int // Session limit for soc
|
||||
limitEnergy float64 // Session limit for energy
|
||||
smartCostLimit *float64 // always charge if cost is below this value
|
||||
batteryBoost int // battery boost state
|
||||
title string // UI title
|
||||
priority int // Priority
|
||||
minCurrent float64 // PV mode: start current Min+PV mode: min current
|
||||
maxCurrent float64 // Max allowed current. Physically ensured by the charger
|
||||
phasesConfigured int // Charger configured phase mode 0/1/3
|
||||
limitSoc int // Session limit for soc
|
||||
limitEnergy float64 // Session limit for energy
|
||||
smartCostLimit *float64 // always charge if consumption cost is below this value
|
||||
smartFeedInPriorityLimit *float64 // prevent charging if feed-in cost is above this value
|
||||
batteryBoost int // battery boost state
|
||||
|
||||
mode api.ChargeMode
|
||||
enabled bool // Charger enabled state
|
||||
|
|
@ -332,6 +333,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.SmartFeedInPriorityLimit); err == nil {
|
||||
lp.SetSmartFeedInPriorityLimit(&v)
|
||||
}
|
||||
|
||||
var thresholds loadpoint.ThresholdsConfig
|
||||
if err := lp.settings.Json(keys.Thresholds, &thresholds); err == nil {
|
||||
|
|
@ -632,6 +636,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
|
|||
lp.publish(keys.ChargerSinglePhase, lp.getChargerPhysicalPhases() == 1)
|
||||
lp.publish(keys.PhasesActive, lp.ActivePhases())
|
||||
lp.publish(keys.SmartCostLimit, lp.smartCostLimit)
|
||||
lp.publish(keys.SmartFeedInPriorityLimit, lp.smartFeedInPriorityLimit)
|
||||
lp.publishTimer(phaseTimer, 0, timerInactive)
|
||||
lp.publishTimer(pvTimer, 0, timerInactive)
|
||||
|
||||
|
|
@ -1781,17 +1786,16 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
|
|||
}
|
||||
|
||||
// Update is the main control function. It reevaluates meters and charger state
|
||||
func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
|
||||
func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, consumption, feedin api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
|
||||
// smart cost
|
||||
smartCostActive := lp.smartCostActive(rates)
|
||||
smartCostActive, smartCostNextStart := lp.checkSmartLimit(lp.GetSmartCostLimit(), consumption, true)
|
||||
lp.publish(keys.SmartCostActive, smartCostActive)
|
||||
|
||||
var smartCostNextStart time.Time
|
||||
if !smartCostActive {
|
||||
smartCostNextStart = lp.smartCostNextStart(rates)
|
||||
}
|
||||
lp.publish(keys.SmartCostNextStart, smartCostNextStart)
|
||||
|
||||
smartFeedInPriorityActive, smartFeedInPriorityNextStart := lp.checkSmartLimit(lp.GetSmartFeedInPriorityLimit(), feedin, false)
|
||||
lp.publish(keys.SmartFeedInPriorityActive, smartFeedInPriorityActive)
|
||||
lp.publish(keys.SmartFeedInPriorityNextStart, smartFeedInPriorityNextStart)
|
||||
|
||||
// long-running tasks
|
||||
lp.processTasks()
|
||||
|
||||
|
|
@ -1902,14 +1906,30 @@ func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rate
|
|||
case mode == api.ModeMinPV || mode == api.ModePV:
|
||||
// cheap tariff
|
||||
if smartCostActive {
|
||||
rate, _ := rates.At(time.Now())
|
||||
lp.log.DEBUG.Printf("smart cost active: %.2f", rate.Value)
|
||||
rate, _ := consumption.At(time.Now())
|
||||
lp.log.DEBUG.Printf("smart consumption active: %.2f", rate.Value)
|
||||
err = lp.fastCharging()
|
||||
lp.resetPhaseTimer()
|
||||
lp.elapsePVTimer() // let PV mode disable immediately afterwards
|
||||
break
|
||||
}
|
||||
|
||||
// attractive feedin
|
||||
if smartFeedInPriorityActive {
|
||||
rate, _ := feedin.At(time.Now())
|
||||
lp.log.DEBUG.Printf("smart feed-in active: %.2f", rate.Value)
|
||||
|
||||
var targetCurrent float64
|
||||
if mode == api.ModeMinPV {
|
||||
targetCurrent = lp.GetMinCurrent()
|
||||
}
|
||||
err = lp.setLimit(targetCurrent)
|
||||
|
||||
lp.resetPhaseTimer()
|
||||
lp.elapsePVTimer() // let PV mode disable immediately afterwards
|
||||
break
|
||||
}
|
||||
|
||||
targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBoostPower, batteryBuffered, batteryStart)
|
||||
|
||||
if targetCurrent == 0 && lp.vehicleClimateActive() {
|
||||
|
|
|
|||
|
|
@ -167,10 +167,14 @@ type API interface {
|
|||
// smart grid charging
|
||||
//
|
||||
|
||||
// GetSmartChargingActive determines if smart charging is active
|
||||
// GetSmartCostLimit return the smart cost limit
|
||||
GetSmartCostLimit() *float64
|
||||
// SetSmartCostLimit sets the smart cost limit
|
||||
SetSmartCostLimit(limit *float64)
|
||||
// GetSmartFeedInPriorityLimit return the smart feed-in limit
|
||||
GetSmartFeedInPriorityLimit() *float64
|
||||
// SetSmartFeedInPriorityLimit sets the smart feed-in limit
|
||||
SetSmartFeedInPriorityLimit(limit *float64)
|
||||
|
||||
//
|
||||
// power and energy
|
||||
|
|
|
|||
|
|
@ -17,18 +17,19 @@ type StaticConfig struct {
|
|||
|
||||
type DynamicConfig struct {
|
||||
// dynamic config
|
||||
Title string `json:"title"`
|
||||
DefaultMode string `json:"defaultMode"`
|
||||
Priority int `json:"priority"`
|
||||
PhasesConfigured int `json:"phasesConfigured"`
|
||||
MinCurrent float64 `json:"minCurrent"`
|
||||
MaxCurrent float64 `json:"maxCurrent"`
|
||||
SmartCostLimit *float64 `json:"smartCostLimit"`
|
||||
PlanEnergy float64 `json:"planEnergy"`
|
||||
PlanTime time.Time `json:"planTime"`
|
||||
PlanPrecondition int64 `json:"planPrecondition"`
|
||||
LimitEnergy float64 `json:"limitEnergy"`
|
||||
LimitSoc int `json:"limitSoc"`
|
||||
Title string `json:"title"`
|
||||
DefaultMode string `json:"defaultMode"`
|
||||
Priority int `json:"priority"`
|
||||
PhasesConfigured int `json:"phasesConfigured"`
|
||||
MinCurrent float64 `json:"minCurrent"`
|
||||
MaxCurrent float64 `json:"maxCurrent"`
|
||||
SmartCostLimit *float64 `json:"smartCostLimit"`
|
||||
SmartFeedInPriorityLimit *float64 `json:"smartFeedInPriorityLimit"`
|
||||
PlanEnergy float64 `json:"planEnergy"`
|
||||
PlanTime time.Time `json:"planTime"`
|
||||
PlanPrecondition int64 `json:"planPrecondition"`
|
||||
LimitEnergy float64 `json:"limitEnergy"`
|
||||
LimitSoc int `json:"limitSoc"`
|
||||
|
||||
Thresholds ThresholdsConfig `json:"thresholds"`
|
||||
Soc SocConfig `json:"soc"`
|
||||
|
|
@ -56,6 +57,7 @@ func (payload DynamicConfig) Apply(lp API) error {
|
|||
lp.SetTitle(payload.Title)
|
||||
lp.SetPriority(payload.Priority)
|
||||
lp.SetSmartCostLimit(payload.SmartCostLimit)
|
||||
lp.SetSmartFeedInPriorityLimit(payload.SmartFeedInPriorityLimit)
|
||||
lp.SetThresholds(payload.Thresholds)
|
||||
lp.SetPlanEnergy(payload.PlanTime, time.Duration(payload.PlanPrecondition)*time.Second, payload.PlanEnergy)
|
||||
lp.SetLimitEnergy(payload.LimitEnergy)
|
||||
|
|
|
|||
|
|
@ -548,6 +548,20 @@ func (mr *MockAPIMockRecorder) GetSmartCostLimit() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).GetSmartCostLimit))
|
||||
}
|
||||
|
||||
// GetSmartFeedInPriorityLimit mocks base method.
|
||||
func (m *MockAPI) GetSmartFeedInPriorityLimit() *float64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetSmartFeedInPriorityLimit")
|
||||
ret0, _ := ret[0].(*float64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetSmartFeedInPriorityLimit indicates an expected call of GetSmartFeedInPriorityLimit.
|
||||
func (mr *MockAPIMockRecorder) GetSmartFeedInPriorityLimit() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartFeedInPriorityLimit", reflect.TypeOf((*MockAPI)(nil).GetSmartFeedInPriorityLimit))
|
||||
}
|
||||
|
||||
// GetSocConfig mocks base method.
|
||||
func (m *MockAPI) GetSocConfig() SocConfig {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
@ -908,6 +922,18 @@ func (mr *MockAPIMockRecorder) SetSmartCostLimit(limit any) *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).SetSmartCostLimit), limit)
|
||||
}
|
||||
|
||||
// SetSmartFeedInPriorityLimit mocks base method.
|
||||
func (m *MockAPI) SetSmartFeedInPriorityLimit(limit *float64) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetSmartFeedInPriorityLimit", limit)
|
||||
}
|
||||
|
||||
// SetSmartFeedInPriorityLimit indicates an expected call of SetSmartFeedInPriorityLimit.
|
||||
func (mr *MockAPIMockRecorder) SetSmartFeedInPriorityLimit(limit any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSmartFeedInPriorityLimit", reflect.TypeOf((*MockAPI)(nil).SetSmartFeedInPriorityLimit), limit)
|
||||
}
|
||||
|
||||
// SetSocConfig mocks base method.
|
||||
func (m *MockAPI) SetSocConfig(soc SocConfig) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
|||
|
|
@ -608,7 +608,8 @@ func (lp *Loadpoint) GetChargePower() float64 {
|
|||
// GetChargePowerFlexibility returns the flexible amount of current charging power
|
||||
func (lp *Loadpoint) GetChargePowerFlexibility(rates api.Rates) float64 {
|
||||
mode := lp.GetMode()
|
||||
if mode == api.ModeNow || !lp.charging() || lp.minSocNotReached() || lp.smartCostActive(rates) {
|
||||
if mode == api.ModeNow || !lp.charging() || lp.minSocNotReached() ||
|
||||
lp.smartLimitActive(lp.GetSmartCostLimit(), rates, true) {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -807,6 +808,28 @@ func (lp *Loadpoint) SetSmartCostLimit(val *float64) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetSmartFeedInPriorityLimit gets the smart feed-in limit
|
||||
func (lp *Loadpoint) GetSmartFeedInPriorityLimit() *float64 {
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
return lp.smartFeedInPriorityLimit
|
||||
}
|
||||
|
||||
// SetSmartFeedInPriorityLimit sets the smart cost feed-in
|
||||
func (lp *Loadpoint) SetSmartFeedInPriorityLimit(val *float64) {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
lp.log.DEBUG.Println("set smart feed-in limit:", printPtr("%.1f", val))
|
||||
|
||||
if !ptrValueEqual(lp.smartFeedInPriorityLimit, val) {
|
||||
lp.smartFeedInPriorityLimit = val
|
||||
|
||||
lp.settings.SetFloatPtr(keys.SmartFeedInPriorityLimit, val)
|
||||
lp.publish(keys.SmartFeedInPriorityLimit, val)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCircuit returns the assigned circuit
|
||||
func (lp *Loadpoint) GetCircuit() api.Circuit {
|
||||
lp.RLock()
|
||||
|
|
|
|||
|
|
@ -6,22 +6,40 @@ import (
|
|||
"github.com/evcc-io/evcc/api"
|
||||
)
|
||||
|
||||
func (lp *Loadpoint) smartCostActive(rates api.Rates) bool {
|
||||
rate, err := rates.At(time.Now())
|
||||
limit := lp.GetSmartCostLimit()
|
||||
return err == nil && limit != nil && rate.Value <= *limit
|
||||
// checkSmartLimit checks if current rate meets smart limit and returns next start time if not active.
|
||||
// checkBelow: true for rate <= limit, false for rate >= limit
|
||||
func (lp *Loadpoint) checkSmartLimit(limit *float64, rates api.Rates, checkBelow bool) (bool, time.Time) {
|
||||
var nextStart time.Time
|
||||
|
||||
active := lp.smartLimitActive(limit, rates, checkBelow)
|
||||
if !active {
|
||||
nextStart = lp.smartLimitNextStart(limit, rates, checkBelow)
|
||||
}
|
||||
|
||||
return active, nextStart
|
||||
}
|
||||
|
||||
// smartCostNextStart returns the next start time for a smart cost rate below the limit
|
||||
func (lp *Loadpoint) smartCostNextStart(rates api.Rates) time.Time {
|
||||
limit := lp.GetSmartCostLimit()
|
||||
func (lp *Loadpoint) smartLimitActive(limit *float64, rates api.Rates, checkBelow bool) bool {
|
||||
rate, err := rates.At(time.Now())
|
||||
if err != nil || limit == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if checkBelow {
|
||||
return rate.Value <= *limit
|
||||
}
|
||||
return rate.Value >= *limit
|
||||
}
|
||||
|
||||
// smartLimitNextStart returns the next start time when the smart limit condition will be met
|
||||
func (lp *Loadpoint) smartLimitNextStart(limit *float64, rates api.Rates, checkBelow bool) time.Time {
|
||||
if limit == nil || rates == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, slot := range rates {
|
||||
if slot.Start.After(now) && slot.Value <= *limit {
|
||||
if slot.Start.After(now) && (checkBelow && slot.Value <= *limit || !checkBelow && slot.Value >= *limit) {
|
||||
return slot.Start
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ func TestUpdatePowerZero(t *testing.T) {
|
|||
}
|
||||
|
||||
lp.mode = tc.mode
|
||||
lp.Update(0, 0, nil, false, false, 0, nil, nil) // false,sitePower false,0
|
||||
lp.Update(0, 0, nil, nil, false, false, 0, nil, nil) // false,sitePower false,0
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
|
|
@ -428,7 +428,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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(500, 0, nil, nil, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
t.Log("charging above target - soc deactivates charger")
|
||||
|
|
@ -437,7 +437,7 @@ 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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(500, 0, nil, nil, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
t.Log("deactivated charger changes status to B")
|
||||
|
|
@ -445,14 +445,14 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
|
|||
vehicle.EXPECT().Soc().Return(95.0, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
lp.Update(-500, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-500, 0, nil, nil, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
t.Log("soc has risen 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(-500, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-500, 0, nil, nil, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
t.Log("soc has fallen below target - soc update timer expired")
|
||||
|
|
@ -462,7 +462,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
|
|||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
|
||||
charger.EXPECT().Enable(true).Return(nil)
|
||||
lp.Update(-500, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-500, 0, nil, nil, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
}
|
||||
|
||||
|
|
@ -497,14 +497,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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(500, 0, nil, nil, false, false, 0, nil, nil)
|
||||
|
||||
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(-300, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-300, 0, nil, nil, false, false, 0, nil, nil)
|
||||
|
||||
if mode := lp.GetMode(); mode != api.ModeOff {
|
||||
t.Error("unexpected mode", mode)
|
||||
|
|
@ -566,14 +566,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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
|
||||
|
||||
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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:00h stop charging at 5 kWh")
|
||||
|
|
@ -581,7 +581,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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:00h restart charging at 5 kWh")
|
||||
|
|
@ -589,7 +589,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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:30h continue charging at 7.5 kWh")
|
||||
|
|
@ -597,7 +597,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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 7500.0)
|
||||
|
||||
t.Log("at 2:00h stop charging at 10 kWh")
|
||||
|
|
@ -605,7 +605,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, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(-1, 0, nil, nil, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 10000.0)
|
||||
|
||||
ctrl.Finish()
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ func TestReconnectVehicle(t *testing.T) {
|
|||
// vehicle not updated yet
|
||||
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)
|
||||
|
||||
lp.Update(0, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(0, 0, nil, nil, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
// detection started
|
||||
|
|
@ -283,7 +283,7 @@ func TestReconnectVehicle(t *testing.T) {
|
|||
// vehicle not updated yet
|
||||
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil)
|
||||
|
||||
lp.Update(0, 0, nil, false, false, 0, nil, nil)
|
||||
lp.Update(0, 0, nil, nil, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
// vehicle detected
|
||||
|
|
|
|||
28
core/site.go
28
core/site.go
|
|
@ -42,7 +42,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(sitePower, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
|
||||
Update(sitePower, batteryBoostPower float64, consumption, feedin api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
|
||||
}
|
||||
|
||||
// measurement is used as slice element for publishing structured data
|
||||
|
|
@ -871,13 +871,18 @@ func (site *Site) update(lp updater) {
|
|||
site.log.DEBUG.Println("----")
|
||||
|
||||
// smart cost and battery mode handling
|
||||
rates, err := site.plannerRates()
|
||||
consumption, err := site.tariffRates(api.TariffUsagePlanner)
|
||||
if err != nil {
|
||||
site.log.WARN.Println("planner:", err)
|
||||
}
|
||||
|
||||
feedin, err := site.tariffRates(api.TariffUsageFeedIn)
|
||||
if err != nil {
|
||||
site.log.WARN.Println("feed-in:", err)
|
||||
}
|
||||
|
||||
// update loadpoints
|
||||
totalChargePower := site.updateLoadpoints(rates)
|
||||
totalChargePower := site.updateLoadpoints(consumption)
|
||||
|
||||
// update all circuits' power and currents
|
||||
if site.circuit != nil {
|
||||
|
|
@ -894,13 +899,13 @@ func (site *Site) update(lp updater) {
|
|||
flexiblePower = site.prioritizer.GetChargePowerFlexibility(lp)
|
||||
}
|
||||
|
||||
rate, err := rates.At(time.Now())
|
||||
if rates != nil && err != nil {
|
||||
rate, err := consumption.At(time.Now())
|
||||
if consumption != nil && err != nil {
|
||||
msg := fmt.Sprintf("no matching rate for: %s", time.Now().Format(time.RFC3339))
|
||||
if len(rates) > 0 {
|
||||
msg += fmt.Sprintf(", %d rates (%s to %s)", len(rates),
|
||||
rates[0].Start.Local().Format(time.RFC3339),
|
||||
rates[len(rates)-1].End.Local().Format(time.RFC3339),
|
||||
if len(consumption) > 0 {
|
||||
msg += fmt.Sprintf(", %d consumption rates (%s to %s)", len(consumption),
|
||||
consumption[0].Start.Local().Format(time.RFC3339),
|
||||
consumption[len(consumption)-1].End.Local().Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -923,8 +928,9 @@ func (site *Site) update(lp updater) {
|
|||
greenShareHome := site.greenShare(0, homePower)
|
||||
greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)
|
||||
|
||||
// TODO
|
||||
lp.Update(
|
||||
sitePower, max(0, site.batteryPower), rates, batteryBuffered, batteryStart,
|
||||
sitePower, max(0, site.batteryPower), consumption, feedin, batteryBuffered, batteryStart,
|
||||
greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints),
|
||||
)
|
||||
|
||||
|
|
@ -962,6 +968,8 @@ func (site *Site) prepare() {
|
|||
site.publish(keys.BatteryMode, site.batteryMode)
|
||||
site.publish(keys.BatteryDischargeControl, site.batteryDischargeControl)
|
||||
site.publish(keys.ResidualPower, site.GetResidualPower())
|
||||
site.publish(keys.SmartCostAvailable, site.isDynamicTariff(api.TariffUsagePlanner))
|
||||
site.publish(keys.SmartFeedInPriorityAvailable, site.isDynamicTariff(api.TariffUsageFeedIn))
|
||||
|
||||
site.publish(keys.Currency, site.tariffs.Currency)
|
||||
if tariff := site.GetTariff(api.TariffUsagePlanner); tariff != nil {
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ func (site *Site) applyBatteryMode(mode api.BatteryMode) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (site *Site) plannerRates() (api.Rates, error) {
|
||||
tariff := site.GetTariff(api.TariffUsagePlanner)
|
||||
func (site *Site) tariffRates(usage api.TariffUsage) (api.Rates, error) {
|
||||
tariff := site.GetTariff(usage)
|
||||
if tariff == nil || tariff.Type() == api.TariffTypePriceStatic {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,3 +167,8 @@ func (site *Site) solarDetails(solar timeseries) solarDetails {
|
|||
|
||||
return res
|
||||
}
|
||||
|
||||
func (site *Site) isDynamicTariff(usage api.TariffUsage) bool {
|
||||
tariff := site.GetTariff(usage)
|
||||
return tariff != nil && tariff.Type() != api.TariffTypePriceStatic
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue