diff --git a/api/api.go b/api/api.go index 171a3f909..681e8b52a 100644 --- a/api/api.go +++ b/api/api.go @@ -64,7 +64,7 @@ type ChargeState interface { Status() (ChargeStatus, error) } -// Charger is able to provide current charging status and to enable/disabler charging +// Charger is able to provide current charging status and enable/disable charging type Charger interface { ChargeState Enabled() (bool, error) diff --git a/core/loadpoint.go b/core/loadpoint.go index acdc9fe79..d8bc862ff 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -90,8 +90,8 @@ type LoadPoint struct { } Enable, Disable ThresholdConfig - MinCurrent int64 // PV mode: start current Min+PV mode: min current - MaxCurrent int64 // Max allowed current. Physically ensured by the charger + MinCurrent float64 // PV mode: start current Min+PV mode: min current + MaxCurrent float64 // Max allowed current. Physically ensured by the charger GuardDuration time.Duration // charger enable/disable minimum holding time enabled bool // Charger enabled state @@ -363,7 +363,7 @@ func (lp *LoadPoint) evChargeCurrentHandler(current float64) { func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current float64) { power := current * float64(lp.Phases) * Voltage - if !lp.enabled || lp.status != api.StatusC { + if !lp.enabled || lp.GetStatus() != api.StatusC { // if disabled we cannot be charging power = 0 } @@ -423,7 +423,7 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even if lp.enabled = enabled; enabled { lp.guardUpdated = lp.clock.Now() // set defined current for use by pv mode - _ = lp.setLimit(float64(lp.MinCurrent), false) + _ = lp.setLimit(lp.GetMinCurrent(), false) } } else { lp.log.ERROR.Printf("charger: %v", err) @@ -435,11 +435,18 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even } } +// syncCharger updates charger status and synchronizes it with expectations func (lp *LoadPoint) syncCharger() { enabled, err := lp.charger.Enabled() - if err == nil && enabled != lp.enabled { - lp.log.WARN.Printf("charger out of sync: expected %vd, got %vd", status[lp.enabled], status[enabled]) - err = lp.charger.Enable(lp.enabled) + if err == nil { + if enabled != lp.enabled { + lp.log.WARN.Printf("charger out of sync: expected %vd, got %vd", status[lp.enabled], status[enabled]) + err = lp.charger.Enable(lp.enabled) + } + + if !enabled && lp.GetStatus() == api.StatusC { + lp.log.WARN.Println("charger logic error: disabled but charging") + } } if err != nil { @@ -447,9 +454,10 @@ func (lp *LoadPoint) syncCharger() { } } +// setLimit applies charger current limits and enables/disables accordingly func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) { // set current - if chargeCurrent != lp.chargeCurrent && chargeCurrent >= float64(lp.MinCurrent) { + if chargeCurrent != lp.chargeCurrent && chargeCurrent >= lp.GetMinCurrent() { if charger, ok := lp.charger.(api.ChargerEx); ok { lp.log.DEBUG.Printf("max charge current: %.2g", chargeCurrent) err = charger.MaxCurrentMillis(chargeCurrent) @@ -467,7 +475,7 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) { } // set enabled - if enabled := chargeCurrent >= float64(lp.MinCurrent); enabled != lp.enabled && err == nil { + if enabled := chargeCurrent >= lp.GetMinCurrent(); enabled != lp.enabled && err == nil { if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 && !force { lp.log.DEBUG.Printf("charger %s: contactor delay %v", status[enabled], remaining) return nil @@ -503,12 +511,20 @@ func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) { // connected returns the EVs connection state func (lp *LoadPoint) connected() bool { - return lp.status == api.StatusB || lp.status == api.StatusC + status := lp.GetStatus() + return status == api.StatusB || status == api.StatusC } // charging returns the EVs charging state func (lp *LoadPoint) charging() bool { - return lp.status == api.StatusC + return lp.GetStatus() == api.StatusC +} + +// charging returns the EVs charging state +func (lp *LoadPoint) setStatus(status api.ChargeStatus) { + lp.Lock() + defer lp.Unlock() + lp.status = status } // targetSocReached checks if target is configured and reached. @@ -662,8 +678,8 @@ func (lp *LoadPoint) updateChargerStatus() error { lp.log.DEBUG.Printf("charger status: %s", status) - if prevStatus := lp.status; status != prevStatus { - lp.status = status + if prevStatus := lp.GetStatus(); status != prevStatus { + lp.setStatus(status) // changed from empty (initial startup) - set connected without sending message if prevStatus == api.StatusNone { @@ -703,9 +719,10 @@ func (lp *LoadPoint) effectiveCurrent() float64 { return lp.chargeCurrents[0] } - if lp.status != api.StatusC { + if lp.GetStatus() != api.StatusC { return 0 } + return lp.chargeCurrent } @@ -719,17 +736,18 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float6 // calculate target charge current from delta power and actual current effectiveCurrent := lp.effectiveCurrent() deltaCurrent := powerToCurrent(-sitePower, lp.Phases) - targetCurrent := math.Max(math.Min(effectiveCurrent+deltaCurrent, float64(lp.MaxCurrent)), 0) + targetCurrent := math.Max(math.Min(effectiveCurrent+deltaCurrent, lp.GetMaxCurrent()), 0) lp.log.DEBUG.Printf("max charge current: %.1fA = %.1fA + %.1fA (%.0fW @ %dp)", targetCurrent, effectiveCurrent, deltaCurrent, sitePower, lp.Phases) // in MinPV mode return at least minCurrent - if mode == api.ModeMinPV && targetCurrent < float64(lp.MinCurrent) { - return float64(lp.MinCurrent) + minCurrent := lp.GetMinCurrent() + if mode == api.ModeMinPV && targetCurrent < minCurrent { + return minCurrent } // read only once to simplify testing - if mode == api.ModePV && lp.enabled && targetCurrent < float64(lp.MinCurrent) { + if mode == api.ModePV && lp.enabled && targetCurrent < minCurrent { // kick off disable sequence if sitePower >= lp.Disable.Threshold { lp.log.DEBUG.Printf("site power %.0fW >= disable threshold %.0fW", sitePower, lp.Disable.Threshold) @@ -748,15 +766,17 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float6 lp.log.DEBUG.Printf("pv disable timer remaining: %v", (lp.Disable.Delay - elapsed).Round(time.Second)) } else { // reset timer + lp.log.DEBUG.Printf("reset pv disable timer: %v", lp.Disable.Delay) lp.pvTimer = lp.clock.Now() } - return float64(lp.MinCurrent) + lp.log.DEBUG.Println("pv enable timer: keep enabled") + return minCurrent } if mode == api.ModePV && !lp.enabled { // kick off enable sequence - if (lp.Enable.Threshold == 0 && targetCurrent >= float64(lp.MinCurrent)) || + if (lp.Enable.Threshold == 0 && targetCurrent >= minCurrent) || (lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) { lp.log.DEBUG.Printf("site power %.0fW < enable threshold %.0fW", sitePower, lp.Enable.Threshold) @@ -768,15 +788,17 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) float6 elapsed := lp.clock.Since(lp.pvTimer) if elapsed >= lp.Enable.Delay { lp.log.DEBUG.Println("pv enable timer elapsed") - return float64(lp.MinCurrent) + return minCurrent } lp.log.DEBUG.Printf("pv enable timer remaining: %v", (lp.Enable.Delay - elapsed).Round(time.Second)) } else { // reset timer + lp.log.DEBUG.Printf("reset pv enable timer: %v", lp.Enable.Delay) lp.pvTimer = lp.clock.Now() } + lp.log.DEBUG.Println("pv enable timer: keep disabled") return 0 } @@ -980,7 +1002,7 @@ func (lp *LoadPoint) Update(sitePower float64) { var targetCurrent float64 // zero disables if lp.climateActive() { lp.log.DEBUG.Println("climater active") - targetCurrent = float64(lp.MinCurrent) + targetCurrent = lp.GetMinCurrent() } err = lp.setLimit(targetCurrent, true) lp.socTimer.Reset() // once SoC is reached, the target charge request is removed @@ -994,11 +1016,11 @@ func (lp *LoadPoint) Update(sitePower float64) { err = lp.setLimit(0, true) case lp.minSocNotReached(): - err = lp.setLimit(float64(lp.MaxCurrent), true) + err = lp.setLimit(lp.GetMaxCurrent(), true) lp.pvDisableTimer() // let PV mode disable immediately afterwards case mode == api.ModeNow: - err = lp.setLimit(float64(lp.MaxCurrent), true) + err = lp.setLimit(lp.GetMaxCurrent(), true) // target charging case lp.socTimer.StartRequired(): @@ -1011,7 +1033,7 @@ func (lp *LoadPoint) Update(sitePower float64) { var required bool // false if targetCurrent == 0 && lp.climateActive() { - targetCurrent = float64(lp.MinCurrent) + targetCurrent = lp.GetMaxCurrent() required = true } diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go index 91a9e9b1b..00e423e84 100644 --- a/core/loadpoint_api.go +++ b/core/loadpoint_api.go @@ -17,6 +17,9 @@ type LoadPointAPI interface { Name() string HasChargeMeter() bool + // status + GetStatus() api.ChargeStatus + // settings GetMode() api.ChargeMode SetMode(api.ChargeMode) @@ -28,10 +31,19 @@ type LoadPointAPI interface { RemoteControl(string, RemoteDemand) // energy - GetMinCurrent() int64 - GetMaxCurrent() int64 - GetMinPower() int64 - GetMaxPower() int64 + GetMinCurrent() float64 + SetMinCurrent(float64) + GetMaxCurrent() float64 + SetMaxCurrent(float64) + GetMinPower() float64 + GetMaxPower() float64 +} + +// GetStatus returns the charging status +func (lp *LoadPoint) GetStatus() api.ChargeStatus { + lp.Lock() + defer lp.Unlock() + return lp.status } // GetMode returns loadpoint charge mode @@ -158,22 +170,48 @@ func (lp *LoadPoint) HasChargeMeter() bool { return lp.chargeMeter != nil && !isWrapped } -// GetMinCurrent returns the minimal loadpoint current -func (lp *LoadPoint) GetMinCurrent() int64 { +// GetMinCurrent returns the min loadpoint current +func (lp *LoadPoint) GetMinCurrent() float64 { + lp.Lock() + defer lp.Unlock() return lp.MinCurrent } -// GetMaxCurrent returns the minimal loadpoint current -func (lp *LoadPoint) GetMaxCurrent() int64 { +// SetMinCurrent returns the min loadpoint current +func (lp *LoadPoint) SetMinCurrent(current float64) { + lp.Lock() + defer lp.Unlock() + + if current != lp.MinCurrent { + lp.MinCurrent = current + lp.publish("minCurrent", lp.MinCurrent) + } +} + +// GetMaxCurrent returns the max loadpoint current +func (lp *LoadPoint) GetMaxCurrent() float64 { + lp.Lock() + defer lp.Unlock() return lp.MaxCurrent } -// GetMinPower returns the minimal loadpoint power for a single phase -func (lp *LoadPoint) GetMinPower() int64 { - return int64(Voltage) * lp.MinCurrent +// SetMaxCurrent returns the max loadpoint current +func (lp *LoadPoint) SetMaxCurrent(current float64) { + lp.Lock() + defer lp.Unlock() + + if current != lp.MaxCurrent { + lp.MaxCurrent = current + lp.publish("maxCurrent", lp.MaxCurrent) + } } -// GetMaxPower returns the minimal loadpoint power taking active phases into account -func (lp *LoadPoint) GetMaxPower() int64 { - return int64(Voltage) * lp.Phases * lp.MaxCurrent +// GetMinPower returns the min loadpoint power for a single phase +func (lp *LoadPoint) GetMinPower() float64 { + return Voltage * lp.GetMinCurrent() +} + +// GetMaxPower returns the max loadpoint power taking active phases into account +func (lp *LoadPoint) GetMaxPower() float64 { + return Voltage * lp.GetMaxCurrent() * float64(lp.Phases) } diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 1e27aa6fa..fa5b9d890 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -15,8 +15,8 @@ import ( ) const ( - minA int64 = 6 - maxA int64 = 16 + minA float64 = 6 + maxA float64 = 16 ) type Null struct{} @@ -62,7 +62,7 @@ func attachListeners(t *testing.T, lp *LoadPoint) { if charger, ok := lp.charger.(*mock.MockCharger); ok && charger != nil { charger.EXPECT().Enabled().Return(true, nil) - charger.EXPECT().MaxCurrent(lp.MinCurrent).Return(nil) + charger.EXPECT().MaxCurrent(int64(lp.MinCurrent)).Return(nil) } lp.Prepare(uiChan, pushChan, lpChan) @@ -111,7 +111,7 @@ func TestUpdatePowerZero(t *testing.T) { h.EXPECT().Enable(false) }}, {api.StatusB, api.ModeNow, func(h *mock.MockCharger) { - h.EXPECT().MaxCurrent(maxA) // true + h.EXPECT().MaxCurrent(int64(maxA)) // true }}, {api.StatusB, api.ModeMinPV, func(h *mock.MockCharger) { // MaxCurrent omitted since identical value @@ -126,7 +126,7 @@ func TestUpdatePowerZero(t *testing.T) { h.EXPECT().Enable(false) }}, {api.StatusC, api.ModeNow, func(h *mock.MockCharger) { - h.EXPECT().MaxCurrent(maxA) // true + h.EXPECT().MaxCurrent(int64(maxA)) // true }}, {api.StatusC, api.ModeMinPV, func(h *mock.MockCharger) { // MaxCurrent omitted since identical value @@ -179,7 +179,7 @@ func TestPVHysteresis(t *testing.T) { type se struct { site float64 delay time.Duration // test case delay since start - current int64 + current float64 } tc := []struct { enabled bool @@ -325,8 +325,8 @@ func TestPVHysteresis(t *testing.T) { lp.enabled = tc.enabled current := lp.pvMaxCurrent(api.ModePV, se.site) - if current != float64(se.current) { - t.Errorf("step %d: wanted %d, got %.f", step, se.current, current) + if current != se.current { + t.Errorf("step %d: wanted %.1f, got %.1f", step, se.current, current) } } @@ -351,10 +351,8 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) { // not connected, test PV mode logic short-circuited lp.status = api.StatusA - // maxCurrent will read actual current in PV mode - // maxCurrent will read enabled state in PV mode - sitePower := -float64(minA*lp.Phases)*Voltage + 1 // 1W below min power + sitePower := -float64(lp.Phases)*minA*Voltage + 1 // 1W below min power current := lp.pvMaxCurrent(api.ModePV, sitePower) if current != 0 { @@ -405,7 +403,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) { vehicle.EXPECT().SoC().Return(85.0, nil) charger.EXPECT().Status().Return(api.StatusC, nil) charger.EXPECT().Enabled().Return(lp.enabled, nil) - charger.EXPECT().MaxCurrent(maxA).Return(nil) + charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil) lp.Update(500) t.Log("charging above target - soc deactivates charger") @@ -474,7 +472,7 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) { t.Log("charging at min") charger.EXPECT().Enabled().Return(lp.enabled, nil) charger.EXPECT().Status().Return(api.StatusC, nil) - charger.EXPECT().MaxCurrent(maxA).Return(nil) + charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil) lp.Update(500) t.Log("switch off when disconnected") diff --git a/core/soc/timer.go b/core/soc/timer.go index da6b4d1b7..b1bd28232 100644 --- a/core/soc/timer.go +++ b/core/soc/timer.go @@ -23,7 +23,7 @@ type Adapter interface { type Timer struct { Adapter log *util.Logger - maxCurrent int64 + maxCurrent float64 current float64 SoC int Time time.Time @@ -32,7 +32,7 @@ type Timer struct { } // NewTimer creates a Timer -func NewTimer(log *util.Logger, adapter Adapter, maxCurrent int64) *Timer { +func NewTimer(log *util.Logger, adapter Adapter, maxCurrent float64) *Timer { lp := &Timer{ log: log, Adapter: adapter, @@ -64,7 +64,7 @@ func (lp *Timer) StartRequired() bool { return false } - power := float64(lp.maxCurrent*lp.ActivePhases()) * lp.Voltage() + power := float64(lp.ActivePhases()) * lp.maxCurrent * lp.Voltage() // time remainingDuration := se.RemainingChargeDuration(power, lp.SoC) diff --git a/hems/ocpp/ocpp.go b/hems/ocpp/ocpp.go index c60ab5165..f7fbd5d65 100644 --- a/hems/ocpp/ocpp.go +++ b/hems/ocpp/ocpp.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/andig/evcc/api" "github.com/andig/evcc/core" "github.com/andig/evcc/hems/ocpp/profile" "github.com/andig/evcc/util" @@ -81,14 +82,12 @@ func (s *OCPP) errorHandler(errC <-chan error) { // Run executes the OCPP chargepoint client func (s *OCPP) Run() { for { - for id := range s.site.LoadPoints() { + for id, lp := range s.site.LoadPoints() { connector := id + 1 status := ocppcore.ChargePointStatusAvailable - if statusP, err := s.cache.GetChecked(id, "charging"); err == nil { - if statusP.Val.(bool) { - status = ocppcore.ChargePointStatusCharging - } + if lp.GetStatus() == api.StatusC { + status = ocppcore.ChargePointStatusCharging } s.log.TRACE.Printf("send: lp-%d status: %+v", connector, status) diff --git a/hems/semp/semp.go b/hems/semp/semp.go index 5a9e89573..cb4399946 100644 --- a/hems/semp/semp.go +++ b/hems/semp/semp.go @@ -428,21 +428,12 @@ func (s *SEMP) allDeviceStatus() (res []DeviceStatus) { return res } +// TODO remove GetChecked function + func (s *SEMP) planningRequest(id int, lp core.LoadPointAPI) (res PlanningRequest) { - mode := api.ModeOff - if modeP, err := s.cache.GetChecked(id, "mode"); err == nil { - mode = modeP.Val.(api.ChargeMode) - } - - var connected bool - if connectedP, err := s.cache.GetChecked(id, "connected"); err == nil { - connected = connectedP.Val.(bool) - } - - var charging bool - if chargingP, err := s.cache.GetChecked(id, "charging"); err == nil { - charging = chargingP.Val.(bool) - } + mode := lp.GetMode() + charging := lp.GetStatus() == api.StatusC + connected := charging || lp.GetStatus() == api.StatusB chargeEstimate := time.Duration(-1) if chargeEstimateP, err := s.cache.GetChecked(id, "chargeEstimate"); err == nil {