- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 725
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split loadpoint and charger handler (#104)
- 0.133.0
- 0.132.1
- 0.132.0
- 0.131.12
- 0.131.11
- 0.131.10
- 0.131.9
- 0.131.8
- 0.131.7
- 0.131.6
- 0.131.5
- 0.131.4
- 0.131.3
- 0.131.2
- 0.131.1
- 0.131.0
- 0.130.13
- 0.130.12
- 0.130.11
- 0.130.10
- 0.130.9
- 0.130.8
- 0.130.7
- 0.130.6
- 0.130.5
- 0.130.4
- 0.130.3
- 0.130.2
- 0.130.1
- 0.130.0
- 0.129.0
- 0.128.4
- 0.128.3
- 0.128.2
- 0.128.1
- 0.128.0
- 0.127.3
- 0.127.2
- 0.127.1
- 0.127.0
- 0.126.6
- 0.126.5
- 0.126.4
- 0.126.3
- 0.126.2
- 0.126.1
- 0.126.0
- 0.125.0
- 0.124.10
- 0.124.9
- 0.124.8
- 0.124.7
- 0.124.6
- 0.124.5
- 0.124.4
- 0.124.2
- 0.124.1
- 0.124.0
- 0.123.9
- 0.123.8
- 0.123.7
- 0.123.6
- 0.123.5
- 0.123.4
- 0.123.3
- 0.123.2
- 0.123.1
- 0.123.0
- 0.122.1
- 0.122.0
- 0.121.5
- 0.121.4
- 0.121.3
- 0.121.2
- 0.121.1
- 0.121.0
- 0.120.3
- 0.120.2
- 0.120.1
- 0.120.0
- 0.119.5
- 0.119.4
- 0.119.3
- 0.119.2
- 0.119.1
- 0.119.0
- 0.118.11
- 0.118.10
- 0.118.9
- 0.118.8
- 0.118.7
- 0.118.6
- 0.118.5
- 0.118.4
- 0.118.3
- 0.118.2
- 0.118.1
- 0.118.0
- 0.117.4
- 0.117.3
- 0.117.2
- 0.117.1
- 0.117.0
- 0.116.7
- 0.116.6
- 0.116.5
- 0.116.4
- 0.116.3
- 0.116.2
- 0.116.1
- 0.116.0
- 0.115.0
- 0.114.1
- 0.114.0
- 0.113.2
- 0.113.1
- 0.113.0
- 0.112.5
- 0.112.4
- 0.112.3
- 0.112.2
- 0.112.1
- 0.112.0
- 0.111.1
- 0.111.0
- 0.110.1
- 0.110.0
- 0.109.2
- 0.109.1
- 0.109.0
- 0.108.3
- 0.108.2
- 0.108.1
- 0.108.0
- 0.107.1
- 0.107.0
- 0.106.5
- 0.106.4
- 0.106.3
- 0.106.2
- 0.106.1
- 0.106.0
- 0.105.2
- 0.105.1
- 0.105.0
- 0.104.2
- 0.104.1
- 0.104
- 0.103
- 0.102.1
- 0.102
- 0.101.3
- 0.101.2
- 0.101.1
- 0.101
- 0.100
- 0.99
- 0.98
- 0.97.1
- 0.97
- 0.96
- 0.95
- 0.94
- 0.93
- 0.92
- 0.91
- 0.90
- 0.89
- 0.88
- 0.87
- 0.86
- 0.85
- 0.84
- 0.83
- 0.82
- 0.81
- 0.80
- 0.79
- 0.78
- 0.77
- 0.76
- 0.75
- 0.74
- 0.73
- 0.72
- 0.71
- 0.70
- 0.69
- 0.68
- 0.67
- 0.66
- 0.65
- 0.64
- 0.63
- 0.62
- 0.61
- 0.60
- 0.59
- 0.58
- 0.57
- 0.56
- 0.55
- 0.54
- 0.53
- 0.52
- 0.51
- 0.50
- 0.49
- 0.48
- 0.47
- 0.46
- 0.45
- 0.44
- 0.43
- 0.42
- 0.41
- 0.40
- 0.39
- 0.38
- 0.37
- 0.36
- 0.35
- 0.34
- 0.33
- 0.32
- 0.31
- 0.30
- 0.29
- 0.28
- 0.27
- 0.26
- 0.25
- 0.24
- 0.23
- 0.22
- 0.21
- 0.20
- 0.19
- 0.18
- 0.17
- 0.16
- 0.15
- 0.14
- 0.13
- 0.12
- 0.11
- 0.10
- 0.9
- 0.8
Showing
5 changed files
with
503 additions
and
133 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters