Split loadpoint and charger handler (#104)
This commit is contained in:
parent
fdfb8dee06
commit
d17289d9ce
5 changed files with 503 additions and 133 deletions
|
|
@ -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"
|
||||
|
|
@ -23,7 +22,7 @@ type config struct {
|
|||
Meters []qualifiedConfig
|
||||
Chargers []qualifiedConfig
|
||||
Vehicles []qualifiedConfig
|
||||
LoadPoints []core.Config
|
||||
LoadPoints []map[string]interface{}
|
||||
}
|
||||
|
||||
type qualifiedConfig struct {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
143
core/chargerhandler.go
Normal file
143
core/chargerhandler.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
332
core/chargerhandler_test.go
Normal file
332
core/chargerhandler_test.go
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/mock"
|
||||
evbus "github.com/asaskevich/EventBus"
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
const (
|
||||
minA int64 = 6
|
||||
maxA int64 = 16
|
||||
sensitivity = 1
|
||||
dt = time.Hour
|
||||
)
|
||||
|
||||
func newChargerHandler(clock clock.Clock, mc api.Charger) ChargerHandler {
|
||||
r := NewChargerHandler("", clock, evbus.New())
|
||||
|
||||
r.charger = mc
|
||||
r.Sensitivity = sensitivity
|
||||
r.guardUpdated = clock.Now()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func TestNewChargerHandler(t *testing.T) {
|
||||
r := NewChargerHandler("", nil, nil)
|
||||
|
||||
if r.MinCurrent != minA {
|
||||
t.Errorf("expected %v, got %v", minA, r.MinCurrent)
|
||||
}
|
||||
if r.MaxCurrent != maxA {
|
||||
t.Errorf("expected %v, got %v", maxA, r.MaxCurrent)
|
||||
}
|
||||
if r.Sensitivity != 10 {
|
||||
t.Errorf("expected %v, got %v", 10, r.Sensitivity)
|
||||
}
|
||||
if r.GuardDuration != 5*time.Minute {
|
||||
t.Errorf("expected %v, got %v", 5*time.Minute, r.GuardDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnable(t *testing.T) {
|
||||
tc := []struct {
|
||||
enabledI bool
|
||||
dt time.Duration
|
||||
enable bool
|
||||
targetCurrentI int64
|
||||
expect func(*mock.MockCharger)
|
||||
}{
|
||||
// any test with current != 0 or min will fail
|
||||
{false, 0, false, 0, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
{false, 0, true, 0, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
{false, dt, true, 0, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().Enable(true).Return(nil)
|
||||
}},
|
||||
{false, 0, true, minA, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
{false, dt, true, minA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().Enable(true).Return(nil)
|
||||
}},
|
||||
{true, 0, false, minA, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
{true, dt, false, minA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().Enable(false).Return(nil)
|
||||
}},
|
||||
{true, 0, true, minA, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
{true, dt, true, minA, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
ctrl := gomock.NewController(t)
|
||||
mc := mock.NewMockCharger(ctrl)
|
||||
|
||||
t.Log(tc)
|
||||
|
||||
clock := clock.NewMock()
|
||||
r := newChargerHandler(clock, mc)
|
||||
r.enabled = tc.enabledI
|
||||
r.targetCurrent = tc.targetCurrentI
|
||||
|
||||
tc.expect(mc)
|
||||
clock.Add(tc.dt)
|
||||
|
||||
if err := r.chargerEnable(tc.enable); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCurrent(t *testing.T) {
|
||||
tc := []struct {
|
||||
targetCurrentI, targetCurrent, targetCurrentO int64
|
||||
expect func(*mock.MockCharger)
|
||||
}{
|
||||
{0, 0, minA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
}},
|
||||
{minA, minA, minA, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
}},
|
||||
{minA, 0, minA, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
}},
|
||||
{minA, maxA, maxA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(maxA).Return(nil)
|
||||
}},
|
||||
{maxA, maxA, maxA, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
}},
|
||||
{minA, 2 * maxA, maxA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(maxA).Return(nil)
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
ctrl := gomock.NewController(t)
|
||||
mc := mock.NewMockCharger(ctrl)
|
||||
|
||||
t.Log(tc)
|
||||
|
||||
clock := clock.NewMock()
|
||||
r := newChargerHandler(clock, mc)
|
||||
r.targetCurrent = tc.targetCurrentI
|
||||
|
||||
tc.expect(mc)
|
||||
|
||||
if err := r.setTargetCurrent(tc.targetCurrent); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if r.targetCurrent != tc.targetCurrentO {
|
||||
t.Errorf("targetCurrent: expected %d, got %d", tc.targetCurrentO, r.targetCurrent)
|
||||
}
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRampOn(t *testing.T) {
|
||||
tc := []struct {
|
||||
enabledI bool
|
||||
targetCurrentI, targetCurrent int64
|
||||
dt time.Duration
|
||||
expect func(*mock.MockCharger)
|
||||
}{
|
||||
// off at zero: set min
|
||||
{false, 0, minA, 0, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
// guard duration
|
||||
}},
|
||||
{false, 0, minA, dt, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
mc.EXPECT().Enable(true).Return(nil)
|
||||
}},
|
||||
// off at max: set min
|
||||
{false, maxA, minA, 0, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
// guard duration
|
||||
}},
|
||||
{false, maxA, minA, dt, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
mc.EXPECT().Enable(true).Return(nil)
|
||||
}},
|
||||
// off at min: set on
|
||||
{false, minA, minA, 0, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
// guard duration
|
||||
}},
|
||||
{false, minA, minA, dt, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
mc.EXPECT().Enable(true).Return(nil)
|
||||
}},
|
||||
// on at min, set min: set min
|
||||
{true, minA, minA, 0, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
// we are enabled: enable call omitted
|
||||
}},
|
||||
// on at max, set min: set min
|
||||
{true, maxA, minA, 0, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
// we are enabled: enable call omitted
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
ctrl := gomock.NewController(t)
|
||||
mc := mock.NewMockCharger(ctrl)
|
||||
|
||||
t.Log(tc)
|
||||
|
||||
clock := clock.NewMock()
|
||||
r := newChargerHandler(clock, mc)
|
||||
r.enabled = tc.enabledI
|
||||
r.targetCurrent = tc.targetCurrentI
|
||||
|
||||
tc.expect(mc)
|
||||
clock.Add(tc.dt)
|
||||
|
||||
if err := r.rampOn(tc.targetCurrent); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRampOff(t *testing.T) {
|
||||
tc := []struct {
|
||||
enabledI bool
|
||||
targetCurrentI int64
|
||||
dt time.Duration
|
||||
expect func(*mock.MockCharger)
|
||||
}{
|
||||
// off at zero
|
||||
{false, 0, 0, func(mc *mock.MockCharger) {
|
||||
// we are off: enable call omitted
|
||||
}},
|
||||
// off at min
|
||||
{false, minA, 0, func(mc *mock.MockCharger) {
|
||||
// we are off: enable call omitted
|
||||
}},
|
||||
// off at max
|
||||
{false, maxA, 0, func(mc *mock.MockCharger) {
|
||||
// we are off: enable call omitted
|
||||
}},
|
||||
// on at min, disable
|
||||
{true, minA, 0, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
// guard duration
|
||||
}},
|
||||
{true, minA, dt, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
mc.EXPECT().Enable(false).Return(nil)
|
||||
}},
|
||||
// on at max, set min
|
||||
{true, maxA, 0, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
// we are not at min: enable call omitted
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
ctrl := gomock.NewController(t)
|
||||
mc := mock.NewMockCharger(ctrl)
|
||||
|
||||
t.Log(tc)
|
||||
|
||||
clock := clock.NewMock()
|
||||
r := newChargerHandler(clock, mc)
|
||||
r.enabled = tc.enabledI
|
||||
r.targetCurrent = tc.targetCurrentI
|
||||
|
||||
tc.expect(mc)
|
||||
clock.Add(tc.dt)
|
||||
|
||||
if err := r.rampOff(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRampUpDown(t *testing.T) {
|
||||
tc := []struct {
|
||||
targetCurrentI, targetCurrent int64
|
||||
expect func(*mock.MockCharger)
|
||||
}{
|
||||
// no change at 0: nop
|
||||
{0, 0, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
// no change at min: nop
|
||||
{minA, minA, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
// at min: set <min
|
||||
{minA, minA - 100, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
// at min: set max
|
||||
{minA, maxA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA + sensitivity).Return(nil)
|
||||
}},
|
||||
// at max: set min
|
||||
{maxA, minA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(maxA - sensitivity).Return(nil)
|
||||
}},
|
||||
// at max: set >max
|
||||
{maxA, maxA + 100, func(mc *mock.MockCharger) {
|
||||
// nop
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
ctrl := gomock.NewController(t)
|
||||
mc := mock.NewMockCharger(ctrl)
|
||||
|
||||
t.Log(tc)
|
||||
|
||||
clock := clock.NewMock()
|
||||
r := newChargerHandler(clock, mc)
|
||||
r.enabled = true
|
||||
r.targetCurrent = tc.targetCurrentI
|
||||
|
||||
tc.expect(mc)
|
||||
|
||||
if err := r.rampUpDown(tc.targetCurrent); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -36,22 +35,17 @@ func powerToCurrent(power, voltage float64, phases int64) int64 {
|
|||
|
||||
// Config contains the public loadpoint configuration
|
||||
type Config struct {
|
||||
Name string
|
||||
Mode api.ChargeMode // Charge mode, guarded by mutex
|
||||
|
||||
// options
|
||||
Sensitivity int64 // Step size of current change
|
||||
Phases int64 // Phases- required for converting power and current.
|
||||
MinCurrent int64 // PV mode: start current Min+PV mode: min current
|
||||
MaxCurrent int64 // Max allowed current. Physically ensured by the charge controller
|
||||
Voltage float64 // Operating voltage. 230V for Germany.
|
||||
ResidualPower float64 // PV meter only: household usage. Grid meter: household safety margin
|
||||
|
||||
ChargerRef string `mapstructure:"charger"` // Charger reference
|
||||
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
|
||||
Meters MetersConfig // Meter references
|
||||
ChargerRef string `mapstructure:"charger"` // Charger reference
|
||||
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
|
||||
|
||||
GuardDuration time.Duration // charger enable/disable minimum holding time
|
||||
Meters MetersConfig // Meter references
|
||||
}
|
||||
|
||||
// MetersConfig contains the loadpoint's meter configuration
|
||||
|
|
@ -72,13 +66,13 @@ type LoadPoint struct {
|
|||
notificationChan chan<- push.Event // notifications
|
||||
uiChan chan<- Param // client push messages
|
||||
|
||||
Config `mapstructure:",squash"` // exposed public configuration
|
||||
Config `mapstructure:",squash"` // exposed public configuration
|
||||
ChargerHandler `mapstructure:",squash"` // handle charger state and current
|
||||
|
||||
chargeTimer api.ChargeTimer
|
||||
chargeRater api.ChargeRater
|
||||
|
||||
// meters
|
||||
charger api.Charger // Charger
|
||||
gridMeter api.Meter // Grid usage meter
|
||||
pvMeter api.Meter // PV generation meter
|
||||
batteryMeter api.Meter // Battery charging meter
|
||||
|
|
@ -86,17 +80,12 @@ type LoadPoint struct {
|
|||
vehicle api.Vehicle // Vehicle
|
||||
|
||||
// cached state
|
||||
status api.ChargeStatus // Charger status
|
||||
targetCurrent int64 // Allowed current. Between MinCurrent and MaxCurrent.
|
||||
enabled bool // Charger enabled state
|
||||
charging bool // Charging cycle
|
||||
gridPower float64 // Grid power
|
||||
pvPower float64 // PV power
|
||||
batteryPower float64 // Battery charge power
|
||||
chargePower float64 // Charging power
|
||||
|
||||
// contactor switch guard
|
||||
guardUpdated time.Time // charger enabled/disabled timestamp
|
||||
status api.ChargeStatus // Charger status
|
||||
charging bool // Charging cycle
|
||||
gridPower float64 // Grid power
|
||||
pvPower float64 // PV power
|
||||
batteryPower float64 // Battery charge power
|
||||
chargePower float64 // Charging power
|
||||
}
|
||||
|
||||
// configProvider gives access to configuration repository
|
||||
|
|
@ -140,23 +129,23 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
|
|||
|
||||
// NewLoadPoint creates a LoadPoint with sane defaults
|
||||
func NewLoadPoint() *LoadPoint {
|
||||
return &LoadPoint{
|
||||
clock: clock.New(),
|
||||
bus: evbus.New(),
|
||||
clock := clock.New()
|
||||
bus := evbus.New()
|
||||
|
||||
lp := &LoadPoint{
|
||||
clock: clock, // mockable time
|
||||
bus: bus, // event bus
|
||||
triggerChan: make(chan struct{}, 1),
|
||||
Config: Config{
|
||||
Name: "main",
|
||||
Mode: api.ModeOff,
|
||||
Phases: 1,
|
||||
Voltage: 230, // V
|
||||
MinCurrent: 6, // A
|
||||
MaxCurrent: 16, // A
|
||||
Sensitivity: 10, // A
|
||||
GuardDuration: 10 * time.Minute,
|
||||
Mode: api.ModeOff,
|
||||
Phases: 1,
|
||||
Voltage: 230, // V
|
||||
},
|
||||
status: api.StatusNone,
|
||||
targetCurrent: 0, // A
|
||||
status: api.StatusNone,
|
||||
ChargerHandler: NewChargerHandler("main", clock, bus),
|
||||
}
|
||||
|
||||
return lp
|
||||
}
|
||||
|
||||
// notify sends push messages to clients
|
||||
|
|
@ -302,32 +291,6 @@ func (lp *LoadPoint) connected() bool {
|
|||
return lp.status == api.StatusB || lp.status == api.StatusC
|
||||
}
|
||||
|
||||
// chargerEnable switches charging on or off. Minimum cycle duration is guaranteed.
|
||||
func (lp *LoadPoint) 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 - time.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 {
|
||||
log.DEBUG.Printf("%s charger %s - contactor delay %v", lp.Name, status[enable], remaining)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := lp.charger.Enable(enable)
|
||||
if err == nil {
|
||||
lp.enabled = enable // cache
|
||||
log.INFO.Printf("%s charger %s", lp.Name, status[enable])
|
||||
lp.guardUpdated = lp.clock.Now()
|
||||
|
||||
// if not enabled, current will be reduced to 0 in handler
|
||||
lp.bus.Publish(evChargeCurrent, lp.MinCurrent)
|
||||
} else {
|
||||
log.DEBUG.Printf("%s charger %s", lp.Name, status[enable])
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// chargingCycle detects charge cycle start and stop events and manages the
|
||||
// charge energy counter and charge timer. It guards against duplicate invocation.
|
||||
func (lp *LoadPoint) chargingCycle(enable bool) {
|
||||
|
|
@ -382,73 +345,6 @@ func (lp *LoadPoint) updateChargeStatus() api.ChargeStatus {
|
|||
return status
|
||||
}
|
||||
|
||||
// setTargetCurrent guards setting current against changing to identical value
|
||||
// and violating MaxCurrent
|
||||
func (lp *LoadPoint) 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
|
||||
func (lp *LoadPoint) 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. If already disables, this is a nop.
|
||||
func (lp *LoadPoint) rampOff() error {
|
||||
if lp.enabled {
|
||||
if lp.targetCurrent == lp.MinCurrent {
|
||||
return lp.chargerEnable(false)
|
||||
}
|
||||
|
||||
return lp.setTargetCurrent(lp.MinCurrent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rampOn enables charger after setting minCurrent. If already enabled, target will be set.
|
||||
func (lp *LoadPoint) 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)
|
||||
}
|
||||
|
||||
// updateModePV sets "minpv" or "pv" load modes
|
||||
func (lp *LoadPoint) updateModePV(mode api.ChargeMode) error {
|
||||
// grid meter will always be available, if as wrapped pv meter
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue