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

Add target charging #370

Merged
merged 3 commits into from
Jan 2, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Refactor estimator and timer into common structure
  • Loading branch information
andig committed Jan 2, 2021
commit 30ea7de43d661d56120f077ddef728e870b747d4
56 changes: 26 additions & 30 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/andig/evcc/api"
"github.com/andig/evcc/core/soc"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/push"
"github.com/andig/evcc/util"
Expand Down Expand Up @@ -94,10 +95,10 @@ type LoadPoint struct {
MaxCurrent int64 // Max allowed current. Physically ensured by the charger
GuardDuration time.Duration // charger enable/disable minimum holding time

enabled bool // Charger enabled state
maxCurrent float64 // Charger current limit
guardUpdated time.Time // Charger enabled/disabled timestamp
socUpdated time.Time // SoC updated timestamp (poll: connected)
enabled bool // Charger enabled state
chargeCurrent float64 // Charger current limit
guardUpdated time.Time // Charger enabled/disabled timestamp
socUpdated time.Time // SoC updated timestamp (poll: connected)

charger api.Charger
chargeTimer api.ChargeTimer
Expand All @@ -106,8 +107,8 @@ type LoadPoint struct {
chargeMeter api.Meter // Charger usage meter
vehicle api.Vehicle // Currently active vehicle
vehicles []api.Vehicle // Assigned vehicles
socEstimator *wrapper.SocEstimator
socTimer *SoCTimer
socEstimator *soc.Estimator
socTimer *soc.Timer

// cached state
status api.ChargeStatus // Charger status
Expand Down Expand Up @@ -193,8 +194,7 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
lp.configureChargerType(lp.charger)

// allow target charge handler to access loadpoint
lp.socTimer = &SoCTimer{LoadPoint: lp}

lp.socTimer = soc.NewTimer(lp.log, lp.adapter(), lp.MaxCurrent)
if lp.Enable.Threshold > lp.Disable.Threshold {
log.WARN.Printf("PV mode enable threshold (%.0fW) is larger than disable threshold (%.0fW)", lp.Enable.Threshold, lp.Disable.Threshold)
}
Expand Down Expand Up @@ -439,23 +439,23 @@ func (lp *LoadPoint) syncCharger() {
}
}

func (lp *LoadPoint) setLimit(maxCurrent float64, force bool) (err error) {
func (lp *LoadPoint) setLimit(chargeCurrent float64, force bool) (err error) {
// set current
if maxCurrent != lp.maxCurrent && maxCurrent >= float64(lp.MinCurrent) {
if chargeCurrent != lp.chargeCurrent && chargeCurrent >= float64(lp.MinCurrent) {
if charger, ok := lp.charger.(api.ChargerEx); ok {
err = charger.MaxCurrentMillis(maxCurrent)
err = charger.MaxCurrentMillis(chargeCurrent)
} else {
err = lp.charger.MaxCurrent(int64(maxCurrent))
err = lp.charger.MaxCurrent(int64(chargeCurrent))
}

if err == nil {
lp.maxCurrent = maxCurrent
lp.bus.Publish(evChargeCurrent, maxCurrent)
lp.chargeCurrent = chargeCurrent
lp.bus.Publish(evChargeCurrent, chargeCurrent)
}
}

// set enabled
if enabled := maxCurrent != 0; enabled != lp.enabled && err == nil {
if enabled := chargeCurrent != 0; 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
Expand All @@ -465,7 +465,7 @@ func (lp *LoadPoint) setLimit(maxCurrent float64, force bool) (err error) {
lp.enabled = enabled
lp.guardUpdated = lp.clock.Now()
lp.log.DEBUG.Printf("charger %s", status[enabled])
lp.bus.Publish(evChargeCurrent, maxCurrent)
lp.bus.Publish(evChargeCurrent, chargeCurrent)
}
}

Expand Down Expand Up @@ -543,7 +543,7 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) {
}

lp.vehicle = vehicle
lp.socEstimator = wrapper.NewSocEstimator(lp.log, vehicle, lp.SoC.Estimate)
lp.socEstimator = soc.NewEstimator(lp.log, vehicle, lp.SoC.Estimate)

lp.publish("socTitle", lp.vehicle.Title())
lp.publish("socCapacity", lp.vehicle.Capacity())
Expand Down Expand Up @@ -619,7 +619,7 @@ func (lp *LoadPoint) updateChargerStatus() error {
}

// update whenever there is a state change
lp.bus.Publish(evChargeCurrent, lp.maxCurrent)
lp.bus.Publish(evChargeCurrent, lp.chargeCurrent)
}

return nil
Expand Down Expand Up @@ -665,7 +665,7 @@ func (lp *LoadPoint) effectiveCurrent() float64 {
if lp.status != api.StatusC {
return 0
}
return lp.maxCurrent
return lp.chargeCurrent
}

// pvDisableTimer puts the pv enable/disable timer into elapsed state
Expand Down Expand Up @@ -862,7 +862,7 @@ func (lp *LoadPoint) Update(sitePower float64) {
lp.updateChargeMeter()

// update ChargeRater here to make sure initial meter update is caught
lp.bus.Publish(evChargeCurrent, lp.maxCurrent)
lp.bus.Publish(evChargeCurrent, lp.chargeCurrent)
lp.bus.Publish(evChargePower, lp.chargePower)

// update progress and soc before status is updated
Expand Down Expand Up @@ -916,16 +916,7 @@ func (lp *LoadPoint) Update(sitePower float64) {
// OCPP has priority over target charging
case lp.remoteControlled(RemoteHardDisable):
remoteDisabled = RemoteHardDisable
err = lp.setLimit(0, true)

// target charging
case lp.socTimer.StartRequired():
var pvCurrent float64
// check if pv mode offers higher current
if mode == api.ModeMinPV || mode == api.ModePV {
pvCurrent = lp.pvMaxCurrent(mode, sitePower)
}
err = lp.socTimer.Handle(pvCurrent)
fallthrough

case mode == api.ModeOff:
err = lp.setLimit(0, true)
Expand All @@ -937,6 +928,11 @@ func (lp *LoadPoint) Update(sitePower float64) {
case mode == api.ModeNow:
err = lp.setLimit(float64(lp.MaxCurrent), true)

// target charging
case lp.socTimer.StartRequired():
targetCurrent := lp.socTimer.Handle()
err = lp.setLimit(targetCurrent, false)

case mode == api.ModeMinPV || mode == api.ModePV:
targetCurrent := lp.pvMaxCurrent(mode, sitePower)
lp.log.DEBUG.Printf("target charge current: %.2gA", targetCurrent)
Expand Down
27 changes: 27 additions & 0 deletions core/loadpoint_adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package core

import "github.com/andig/evcc/core/soc"

type adapter struct {
lp *LoadPoint
}

func (lp *LoadPoint) adapter() soc.Adapter {
return &adapter{lp: lp}
}

func (a *adapter) Publish(key string, val interface{}) {
a.lp.publish(key, val)
}

func (a *adapter) SocEstimator() *soc.Estimator {
return a.lp.socEstimator
}

func (a *adapter) ActivePhases() int64 {
return a.lp.Phases
}

func (a *adapter) Voltage() float64 {
return Voltage
}
10 changes: 5 additions & 5 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"time"

"github.com/andig/evcc/api"
"github.com/andig/evcc/core/wrapper"
"github.com/andig/evcc/core/soc"
"github.com/andig/evcc/mock"
"github.com/andig/evcc/push"
"github.com/andig/evcc/util"
Expand Down Expand Up @@ -365,7 +365,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {

// wrap vehicle with estimator
vehicle.EXPECT().Capacity().Return(int64(10))
socEstimator := wrapper.NewSocEstimator(util.NewLogger("foo"), vehicle, false)
socEstimator := soc.NewEstimator(util.NewLogger("foo"), vehicle, false)

lp := &LoadPoint{
log: util.NewLogger("foo"),
Expand All @@ -392,7 +392,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
attachListeners(t, lp)

lp.enabled = true
lp.maxCurrent = float64(minA)
lp.chargeCurrent = float64(minA)

t.Log("charging below soc target")
vehicle.EXPECT().ChargeState().Return(85.0, nil)
Expand Down Expand Up @@ -461,7 +461,7 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
attachListeners(t, lp)

lp.enabled = true
lp.maxCurrent = float64(minA)
lp.chargeCurrent = float64(minA)
lp.Mode = api.ModeNow

t.Log("charging at min")
Expand Down Expand Up @@ -526,7 +526,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
attachListeners(t, lp)

lp.enabled = true
lp.maxCurrent = float64(maxA)
lp.chargeCurrent = float64(maxA)
lp.Mode = api.ModeNow

// attach cache for verifying values
Expand Down
20 changes: 10 additions & 10 deletions core/wrapper/socestimator.go → core/soc/socestimator.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package wrapper
package soc

import (
"errors"
Expand All @@ -11,9 +11,9 @@ import (

const chargeEfficiency = 0.9 // assume charge 90% efficiency

// SocEstimator provides vehicle soc and charge duration
// Estimator provides vehicle soc and charge duration
// Vehicle SoC can be estimated to provide more granularity
type SocEstimator struct {
type Estimator struct {
log *util.Logger
vehicle api.Vehicle
estimate bool
Expand All @@ -26,9 +26,9 @@ type SocEstimator struct {
energyPerSocStep float64 // Energy per SoC percent in Wh
}

// NewSocEstimator creates new estimator
func NewSocEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *SocEstimator {
s := &SocEstimator{
// NewEstimator creates new estimator
func NewEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *Estimator {
s := &Estimator{
log: log,
vehicle: vehicle,
estimate: estimate,
Expand All @@ -40,7 +40,7 @@ func NewSocEstimator(log *util.Logger, vehicle api.Vehicle, estimate bool) *SocE
}

// Reset resets the estimation process to default values
func (s *SocEstimator) Reset() {
func (s *Estimator) Reset() {
s.prevSoC = 0
s.prevChargedEnergy = 0
s.capacity = float64(s.vehicle.Capacity()) * 1e3 // cache to simplify debugging
Expand All @@ -49,7 +49,7 @@ func (s *SocEstimator) Reset() {
}

// RemainingChargeDuration returns the remaining duration estimate based on SoC, target and charge power
func (s *SocEstimator) RemainingChargeDuration(chargePower float64, targetSoC int) time.Duration {
func (s *Estimator) RemainingChargeDuration(chargePower float64, targetSoC int) time.Duration {
if chargePower > 0 {
percentRemaining := float64(targetSoC) - s.socCharge
if percentRemaining <= 0 {
Expand Down Expand Up @@ -78,7 +78,7 @@ func (s *SocEstimator) RemainingChargeDuration(chargePower float64, targetSoC in
}

// RemainingChargeEnergy returns the remaining charge energy in kWh
func (s *SocEstimator) RemainingChargeEnergy(targetSoC int) float64 {
func (s *Estimator) RemainingChargeEnergy(targetSoC int) float64 {
percentRemaining := float64(targetSoC) - s.socCharge
if percentRemaining <= 0 {
return 0
Expand All @@ -90,7 +90,7 @@ func (s *SocEstimator) RemainingChargeEnergy(targetSoC int) float64 {
}

// SoC implements Vehicle.ChargeState with addition of given charged energy
func (s *SocEstimator) SoC(chargedEnergy float64) (float64, error) {
func (s *Estimator) SoC(chargedEnergy float64) (float64, error) {
f, err := s.vehicle.ChargeState()
if err != nil {
s.log.WARN.Printf("updating soc failed: %v", err)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package wrapper
package soc

import (
"testing"
Expand All @@ -15,7 +15,7 @@ func TestRemainingChargeDuration(t *testing.T) {
//9 kWh userBatCap => 10 kWh virtualBatCap
vehicle.EXPECT().Capacity().Return(int64(9))

ce := NewSocEstimator(util.NewLogger("foo"), vehicle, false)
ce := NewEstimator(util.NewLogger("foo"), vehicle, false)
ce.socCharge = 20.0

chargePower := 1000.0
Expand All @@ -34,7 +34,7 @@ func TestSoCEstimation(t *testing.T) {
var capacity int64 = 9
vehicle.EXPECT().Capacity().Return(capacity)

ce := NewSocEstimator(util.NewLogger("foo"), vehicle, true)
ce := NewEstimator(util.NewLogger("foo"), vehicle, true)
ce.socCharge = 20.0

tc := []struct {
Expand Down
Loading