Skip to content

Commit

Permalink
Split loadpoint and charger handler (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed May 6, 2020
1 parent fdfb8de commit d17289d
Show file tree
Hide file tree
Showing 5 changed files with 503 additions and 133 deletions.
3 changes: 1 addition & 2 deletions cmd/config.go
Expand Up @@ -5,7 +5,6 @@ import (

"github.com/andig/evcc/api"
"github.com/andig/evcc/charger"
"github.com/andig/evcc/core"
"github.com/andig/evcc/meter"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
Expand All @@ -23,7 +22,7 @@ type config struct {
Meters []qualifiedConfig
Chargers []qualifiedConfig
Vehicles []qualifiedConfig
LoadPoints []core.Config
LoadPoints []map[string]interface{}
}

type qualifiedConfig struct {
Expand Down
6 changes: 3 additions & 3 deletions cmd/setup.go
Expand Up @@ -45,10 +45,10 @@ func loadConfig(conf config, eventsChan chan push.Event) (loadPoints []*core.Loa
}

// decode slice into slice of maps
var lpm []map[string]interface{}
util.DecodeOther(log, lps, &lpm)
var lpc []map[string]interface{}
util.DecodeOther(log, lps, &lpc)

for _, lpc := range lpm {
for _, lpc := range lpc {
lp := core.NewLoadPointFromConfig(log, cp, lpc)
loadPoints = append(loadPoints, lp)
}
Expand Down
143 changes: 143 additions & 0 deletions core/chargerhandler.go
@@ -0,0 +1,143 @@
package core

import (
"fmt"
"time"

"github.com/andig/evcc/api"

evbus "github.com/asaskevich/EventBus"
"github.com/benbjohnson/clock"
)

// ChargerHandler handles steering of the charger state and allowed current
type ChargerHandler struct {
clock clock.Clock // mockable time
bus evbus.Bus // event bus

charger api.Charger // Charger

Name string
Sensitivity int64 // Step size of current change
MinCurrent int64 // PV mode: start current Min+PV mode: min current
MaxCurrent int64 // Max allowed current. Physically ensured by the charge controller
GuardDuration time.Duration // charger enable/disable minimum holding time

enabled bool // Charger enabled state
targetCurrent int64

// contactor switch guard
guardUpdated time.Time // charger enabled/disabled timestamp
}

// NewChargerHandler creates handler with default settings
func NewChargerHandler(name string, clock clock.Clock, bus evbus.Bus) ChargerHandler {
return ChargerHandler{
Name: name,
clock: clock, // mockable time
bus: bus, // event bus
MinCurrent: 6, // A
MaxCurrent: 16, // A
Sensitivity: 10, // A
GuardDuration: 5 * time.Minute,
}
}

// chargerEnable switches charging on or off. Minimum cycle duration is guaranteed.
func (lp *ChargerHandler) chargerEnable(enable bool) error {
if lp.targetCurrent != 0 && lp.targetCurrent != lp.MinCurrent {
log.FATAL.Fatal("charger enable/disable called without setting min current first")
}

if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 {
log.DEBUG.Printf("%s charger %s - contactor delay %v", lp.Name, status[enable], remaining)
return nil
}

if lp.enabled != enable {
if err := lp.charger.Enable(enable); err != nil {
return fmt.Errorf("%s charge controller error: %v", lp.Name, err)
}

lp.enabled = enable // cache
log.INFO.Printf("%s charger %s", lp.Name, status[enable])
lp.guardUpdated = lp.clock.Now()
} else {
log.DEBUG.Printf("%s charger %s", lp.Name, status[enable])
}

// if not enabled, current will be reduced to 0 in handler
lp.bus.Publish(evChargeCurrent, lp.MinCurrent)

return nil
}

// setTargetCurrent guards setting current against changing to identical value
// and violating MaxCurrent
func (lp *ChargerHandler) setTargetCurrent(targetCurrentIn int64) error {
targetCurrent := clamp(targetCurrentIn, lp.MinCurrent, lp.MaxCurrent)
if targetCurrent != targetCurrentIn {
log.WARN.Printf("%s hard limit charge current: %dA", lp.Name, targetCurrent)
}

if lp.targetCurrent != targetCurrent {
log.DEBUG.Printf("%s set charge current: %dA", lp.Name, targetCurrent)
if err := lp.charger.MaxCurrent(targetCurrent); err != nil {
return fmt.Errorf("%s charge controller error: %v", lp.Name, err)
}

lp.targetCurrent = targetCurrent // cache
}

lp.bus.Publish(evChargeCurrent, targetCurrent)

return nil
}

// rampUpDown moves stepwise towards target current.
// It does not enable or disable the charger.
func (lp *ChargerHandler) rampUpDown(target int64) error {
current := lp.targetCurrent
if current == target {
return nil
}

var step int64
if current < target {
step = min(current+lp.Sensitivity, target)
} else if current > target {
step = max(current-lp.Sensitivity, target)
}

step = clamp(step, lp.MinCurrent, lp.MaxCurrent)

return lp.setTargetCurrent(step)
}

// rampOff disables charger after setting minCurrent.
// Setting current and disabling are two steps. If already disabled, this is a nop.
func (lp *ChargerHandler) rampOff() error {
if lp.enabled {
if lp.targetCurrent == lp.MinCurrent {
return lp.chargerEnable(false)
}

return lp.setTargetCurrent(lp.MinCurrent)
}

return nil
}

// rampOn enables charger immediately after setting minCurrent.
// If already enabled, target will be set.
func (lp *ChargerHandler) rampOn(target int64) error {
if !lp.enabled {
if err := lp.setTargetCurrent(lp.MinCurrent); err != nil {
return err
}

return lp.chargerEnable(true)
}

return lp.setTargetCurrent(target)
}

0 comments on commit d17289d

Please sign in to comment.