Refactor charger handler part 2 (#482)
This commit is contained in:
parent
4f06e6948d
commit
10b206a912
6 changed files with 245 additions and 880 deletions
|
|
@ -1,171 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/util"
|
||||
|
||||
evbus "github.com/asaskevich/EventBus"
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
//go:generate mockgen -package mock -destination ../mock/mock_chargerhandler.go github.com/andig/evcc/core Handler
|
||||
|
||||
// Handler is the charger handler responsible for enabled state, target current and guard durations
|
||||
type Handler interface {
|
||||
Prepare()
|
||||
Sync()
|
||||
Enabled() bool
|
||||
Status() (api.ChargeStatus, error)
|
||||
TargetCurrent() int64
|
||||
Ramp(int64, bool) error
|
||||
}
|
||||
|
||||
// HandlerConfig contains the public configuration for the ChargerHandler
|
||||
type HandlerConfig struct {
|
||||
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
|
||||
}
|
||||
|
||||
// ChargerHandler handles steering of the charger state and allowed current
|
||||
type ChargerHandler struct {
|
||||
clock clock.Clock // mockable time
|
||||
bus evbus.Bus // event bus
|
||||
log *util.Logger
|
||||
|
||||
charger api.Charger // Charger
|
||||
|
||||
HandlerConfig // public configuration
|
||||
|
||||
enabled bool // Charger enabled state
|
||||
targetCurrent int64 // Charger target current
|
||||
|
||||
// contactor switch guard
|
||||
guardUpdated time.Time // charger enabled/disabled timestamp
|
||||
}
|
||||
|
||||
// Status returns charger status
|
||||
func (lp *ChargerHandler) Status() (api.ChargeStatus, error) {
|
||||
return lp.charger.Status()
|
||||
}
|
||||
|
||||
// Enabled returns handler enabled state
|
||||
func (lp *ChargerHandler) Enabled() bool {
|
||||
return lp.enabled
|
||||
}
|
||||
|
||||
// TargetCurrent returns handler target current
|
||||
func (lp *ChargerHandler) TargetCurrent() int64 {
|
||||
return lp.targetCurrent
|
||||
}
|
||||
|
||||
// Prepare synchronizes initial charger enabled state and current
|
||||
func (lp *ChargerHandler) Prepare() {
|
||||
// read initial enabled state
|
||||
enabled, err := lp.charger.Enabled()
|
||||
if err == nil {
|
||||
lp.enabled = enabled
|
||||
lp.log.INFO.Printf("charger %sd", status[lp.enabled])
|
||||
|
||||
// prevent immediately disabling charger
|
||||
if lp.enabled {
|
||||
lp.guardUpdated = lp.clock.Now()
|
||||
}
|
||||
} else {
|
||||
lp.log.ERROR.Printf("charger error: %v", err)
|
||||
}
|
||||
|
||||
// set current to known value
|
||||
if err = lp.setTargetCurrent(lp.MinCurrent); err != nil {
|
||||
lp.log.ERROR.Println(err)
|
||||
}
|
||||
lp.bus.Publish(evChargeCurrent, lp.MinCurrent)
|
||||
}
|
||||
|
||||
// Sync synchronizes charger settings to expected state
|
||||
func (lp *ChargerHandler) Sync() {
|
||||
enabled, err := lp.charger.Enabled()
|
||||
if err == nil && enabled != lp.enabled {
|
||||
lp.log.WARN.Printf("sync enabled state to %s", status[lp.enabled])
|
||||
err = lp.charger.Enable(lp.enabled)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
lp.log.ERROR.Printf("charge controller error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// chargerEnable switches charging on or off. Minimum cycle duration is guaranteed.
|
||||
func (lp *ChargerHandler) chargerEnable(enable bool) error {
|
||||
if remaining := (lp.GuardDuration - lp.clock.Since(lp.guardUpdated)).Truncate(time.Second); remaining > 0 {
|
||||
lp.log.DEBUG.Printf("charger %s - contactor delay %v", status[enable], remaining)
|
||||
return nil
|
||||
}
|
||||
|
||||
if lp.enabled != enable {
|
||||
if err := lp.charger.Enable(enable); err != nil {
|
||||
return fmt.Errorf("charge controller error: %v", err)
|
||||
}
|
||||
|
||||
lp.enabled = enable // cache
|
||||
lp.log.INFO.Printf("charger %s", status[enable])
|
||||
lp.guardUpdated = lp.clock.Now()
|
||||
} else {
|
||||
lp.log.DEBUG.Printf("charger %s", 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(targetCurrent int64) error {
|
||||
target := clamp(targetCurrent, lp.MinCurrent, lp.MaxCurrent)
|
||||
|
||||
if lp.targetCurrent != target {
|
||||
lp.log.DEBUG.Printf("set charge current: %dA", target)
|
||||
if err := lp.charger.MaxCurrent(target); err != nil {
|
||||
return fmt.Errorf("charge controller error: %v", err)
|
||||
}
|
||||
|
||||
lp.targetCurrent = target // cache
|
||||
}
|
||||
|
||||
// if not enabled, current will be reduced to 0 in handler
|
||||
lp.bus.Publish(evChargeCurrent, target)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ramp performs ramping charger current up and down where targetCurrent=0
|
||||
// signals disabled state
|
||||
func (lp *ChargerHandler) Ramp(targetCurrent int64, force bool) error {
|
||||
// reset guard updated
|
||||
if force {
|
||||
lp.guardUpdated = time.Time{}
|
||||
}
|
||||
|
||||
// if targetCurrent == 0 disable
|
||||
if targetCurrent == 0 {
|
||||
err := lp.chargerEnable(false)
|
||||
if err == nil && lp.enabled && lp.targetCurrent > lp.MinCurrent {
|
||||
err = lp.setTargetCurrent(lp.MinCurrent)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// else set targetCurrent and optionally enable
|
||||
err := lp.setTargetCurrent(targetCurrent)
|
||||
if err == nil && !lp.enabled {
|
||||
err = lp.chargerEnable(true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/mock"
|
||||
"github.com/andig/evcc/util"
|
||||
evbus "github.com/asaskevich/EventBus"
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
const (
|
||||
minA int64 = 6
|
||||
maxA int64 = 16
|
||||
guardDuration = 5 * time.Minute
|
||||
dt = time.Hour
|
||||
)
|
||||
|
||||
func newChargerHandler(clock clock.Clock, mc api.Charger) *ChargerHandler {
|
||||
h := &ChargerHandler{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clock,
|
||||
bus: evbus.New(),
|
||||
charger: mc,
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
GuardDuration: guardDuration,
|
||||
},
|
||||
}
|
||||
|
||||
// prepare charger and set guardUpdated
|
||||
mc.(*mock.MockCharger).EXPECT().Enabled().Return(true, nil)
|
||||
mc.(*mock.MockCharger).EXPECT().MaxCurrent(int64(6)).Return(nil)
|
||||
h.Prepare()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// test here to ensure loadpoint defaults are valid
|
||||
func TestNewChargerHandler(t *testing.T) {
|
||||
// LoadPoint contains ChargerHandler configuration
|
||||
r := NewLoadPoint(util.NewLogger("foo"))
|
||||
|
||||
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.GuardDuration != guardDuration {
|
||||
t.Errorf("expected %v, got %v", guardDuration, r.GuardDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnable(t *testing.T) {
|
||||
tc := []struct {
|
||||
enabled bool
|
||||
dt time.Duration
|
||||
enable bool
|
||||
targetCurrent 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.enabled
|
||||
r.targetCurrent = tc.targetCurrent
|
||||
|
||||
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.Ramp(tc.targetCurrent, false); 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) {
|
||||
// guard duration
|
||||
}},
|
||||
// on at min, set disable
|
||||
{true, minA, dt, func(mc *mock.MockCharger) {
|
||||
// we are at min: current call omitted
|
||||
mc.EXPECT().Enable(false).Return(nil)
|
||||
}},
|
||||
// on at max, set disable
|
||||
{true, maxA, 0, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).Return(nil)
|
||||
// guard duration
|
||||
}},
|
||||
// on at max, set disable
|
||||
{true, maxA, dt, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().Enable(false).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.enabled = tc.enabledI
|
||||
r.targetCurrent = tc.targetCurrentI
|
||||
|
||||
tc.expect(mc)
|
||||
clock.Add(tc.dt)
|
||||
|
||||
if err := r.Ramp(0, false); 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(maxA).Return(nil)
|
||||
}},
|
||||
// at max: set min
|
||||
{maxA, minA, func(mc *mock.MockCharger) {
|
||||
mc.EXPECT().MaxCurrent(minA).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()
|
||||
h := newChargerHandler(clock, mc)
|
||||
h.enabled = true
|
||||
h.targetCurrent = tc.targetCurrentI
|
||||
|
||||
tc.expect(mc)
|
||||
|
||||
if err := h.Ramp(tc.targetCurrent, false); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -73,9 +73,15 @@ type LoadPoint struct {
|
|||
}
|
||||
Enable, Disable ThresholdConfig
|
||||
|
||||
handler Handler
|
||||
HandlerConfig `mapstructure:",squash"` // handle charger state and current
|
||||
MinCurrent int64 // PV mode: start current Min+PV mode: min current
|
||||
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 int64 // Charger current limit
|
||||
guardUpdated time.Time // Charger enabled/disabled timestamp
|
||||
|
||||
charger api.Charger
|
||||
chargeTimer api.ChargeTimer
|
||||
chargeRater api.ChargeRater
|
||||
|
||||
|
|
@ -139,21 +145,13 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
|
|||
if lp.ChargerRef == "" {
|
||||
return nil, errors.New("missing charger")
|
||||
}
|
||||
charger := cp.Charger(lp.ChargerRef)
|
||||
lp.configureChargerType(charger)
|
||||
lp.charger = cp.Charger(lp.ChargerRef)
|
||||
lp.configureChargerType(lp.charger)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
lp.handler = &ChargerHandler{
|
||||
log: lp.log,
|
||||
clock: lp.clock,
|
||||
bus: lp.bus,
|
||||
charger: charger,
|
||||
HandlerConfig: lp.HandlerConfig,
|
||||
}
|
||||
|
||||
return lp, nil
|
||||
}
|
||||
|
||||
|
|
@ -163,17 +161,15 @@ func NewLoadPoint(log *util.Logger) *LoadPoint {
|
|||
bus := evbus.New()
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: log, // logger
|
||||
clock: clock, // mockable time
|
||||
bus: bus, // event bus
|
||||
Mode: api.ModeOff,
|
||||
Phases: 1,
|
||||
status: api.StatusNone,
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: 6, // A
|
||||
MaxCurrent: 16, // A
|
||||
GuardDuration: 5 * time.Minute,
|
||||
},
|
||||
log: log, // logger
|
||||
clock: clock, // mockable time
|
||||
bus: bus, // event bus
|
||||
Mode: api.ModeOff,
|
||||
Phases: 1,
|
||||
status: api.StatusNone,
|
||||
MinCurrent: 6, // A
|
||||
MaxCurrent: 16, // A
|
||||
GuardDuration: 5 * time.Minute,
|
||||
}
|
||||
|
||||
return lp
|
||||
|
|
@ -301,7 +297,7 @@ func (lp *LoadPoint) evChargeCurrentHandler(current int64) {
|
|||
func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current int64) {
|
||||
power := float64(current*lp.Phases) * Voltage
|
||||
|
||||
if !lp.handler.Enabled() || lp.status != api.StatusC {
|
||||
if !lp.enabled || lp.status != api.StatusC {
|
||||
// if disabled we cannot be charging
|
||||
power = 0
|
||||
}
|
||||
|
|
@ -357,8 +353,56 @@ func (lp *LoadPoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
lp.setActiveVehicle(lp.vehicles[0])
|
||||
}
|
||||
|
||||
// read initial charger state to prevent immediately disabling charger
|
||||
if enabled, err := lp.charger.Enabled(); err == nil {
|
||||
if lp.enabled = enabled; enabled {
|
||||
lp.guardUpdated = lp.clock.Now()
|
||||
}
|
||||
} else {
|
||||
lp.log.ERROR.Printf("charger error: %v", err)
|
||||
}
|
||||
|
||||
// prepare charger status
|
||||
lp.handler.Prepare()
|
||||
lp.setLimit(lp.MinCurrent, false)
|
||||
}
|
||||
|
||||
func (lp *LoadPoint) syncCharger() {
|
||||
enabled, err := lp.charger.Enabled()
|
||||
if err == nil && enabled != lp.enabled {
|
||||
lp.log.WARN.Println("charger out of sync")
|
||||
err = lp.charger.Enable(lp.enabled)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
lp.log.ERROR.Printf("charger error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *LoadPoint) setLimit(maxCurrent int64, force bool) (err error) {
|
||||
// set current
|
||||
if maxCurrent != lp.maxCurrent && maxCurrent >= lp.MinCurrent {
|
||||
if err = lp.charger.MaxCurrent(maxCurrent); err == nil {
|
||||
lp.maxCurrent = maxCurrent
|
||||
lp.bus.Publish(evChargeCurrent, maxCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
// set enabled
|
||||
if enabled := maxCurrent != 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
|
||||
}
|
||||
|
||||
if err = lp.charger.Enable(enabled); err == nil {
|
||||
lp.enabled = enabled
|
||||
lp.guardUpdated = lp.clock.Now()
|
||||
lp.log.DEBUG.Printf("charger %s", status[enabled])
|
||||
lp.bus.Publish(evChargeCurrent, maxCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// connected returns the EVs connection state
|
||||
|
|
@ -475,7 +519,7 @@ func (lp *LoadPoint) findActiveVehicle() {
|
|||
|
||||
// updateChargerStatus updates charger status and detects car connected/disconnected events
|
||||
func (lp *LoadPoint) updateChargerStatus() error {
|
||||
status, err := lp.handler.Status()
|
||||
status, err := lp.charger.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -503,7 +547,7 @@ func (lp *LoadPoint) updateChargerStatus() error {
|
|||
}
|
||||
|
||||
// update whenever there is a state change
|
||||
lp.bus.Publish(evChargeCurrent, lp.handler.TargetCurrent())
|
||||
lp.bus.Publish(evChargeCurrent, lp.maxCurrent)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -551,7 +595,7 @@ func (lp *LoadPoint) pvDisableTimer() {
|
|||
// pvMaxCurrent calculates the maximum target current for PV mode
|
||||
func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) int64 {
|
||||
// calculate target charge current from delta power and actual current
|
||||
effectiveCurrent := lp.handler.TargetCurrent()
|
||||
effectiveCurrent := lp.maxCurrent
|
||||
if lp.status != api.StatusC {
|
||||
effectiveCurrent = 0
|
||||
}
|
||||
|
|
@ -577,9 +621,7 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) int64
|
|||
}
|
||||
|
||||
// read only once to simplify testing
|
||||
enabled := lp.handler.Enabled()
|
||||
|
||||
if mode == api.ModePV && enabled && targetCurrent < lp.MinCurrent {
|
||||
if mode == api.ModePV && lp.enabled && targetCurrent < lp.MinCurrent {
|
||||
// kick off disable sequence
|
||||
if sitePower >= lp.Disable.Threshold {
|
||||
lp.log.DEBUG.Printf("site power %.0fW >= disable threshold %.0fW", sitePower, lp.Disable.Threshold)
|
||||
|
|
@ -604,7 +646,7 @@ func (lp *LoadPoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64) int64
|
|||
return lp.MinCurrent
|
||||
}
|
||||
|
||||
if mode == api.ModePV && !enabled {
|
||||
if mode == api.ModePV && !lp.enabled {
|
||||
// kick off enable sequence
|
||||
if targetCurrent >= lp.MinCurrent ||
|
||||
(lp.Enable.Threshold != 0 && sitePower <= lp.Enable.Threshold) {
|
||||
|
|
@ -717,7 +759,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.handler.TargetCurrent())
|
||||
lp.bus.Publish(evChargeCurrent, lp.maxCurrent)
|
||||
lp.bus.Publish(evChargePower, lp.chargePower)
|
||||
|
||||
// update progress and soc before status is updated
|
||||
|
|
@ -725,7 +767,7 @@ func (lp *LoadPoint) Update(sitePower float64) {
|
|||
|
||||
// read and publish status
|
||||
if err := lp.updateChargerStatus(); err != nil {
|
||||
lp.log.ERROR.Printf("charge controller error: %v", err)
|
||||
lp.log.ERROR.Printf("charger error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -739,9 +781,7 @@ func (lp *LoadPoint) Update(sitePower float64) {
|
|||
lp.publishSoC()
|
||||
|
||||
// sync settings with charger
|
||||
if lp.status != api.StatusA {
|
||||
lp.handler.Sync()
|
||||
}
|
||||
lp.syncCharger()
|
||||
|
||||
// phase detection
|
||||
lp.detectPhases()
|
||||
|
|
@ -757,14 +797,14 @@ func (lp *LoadPoint) Update(sitePower float64) {
|
|||
case !lp.connected():
|
||||
// always disable charger if not connected
|
||||
// https://github.com/andig/evcc/issues/105
|
||||
err = lp.handler.Ramp(0, false)
|
||||
err = lp.setLimit(0, false)
|
||||
|
||||
case lp.targetSocReached():
|
||||
var targetCurrent int64 // zero disables
|
||||
if lp.climateActive() {
|
||||
targetCurrent = lp.MinCurrent
|
||||
}
|
||||
err = lp.handler.Ramp(targetCurrent, true)
|
||||
err = lp.setLimit(targetCurrent, true)
|
||||
|
||||
// OCPP
|
||||
case lp.remoteControlled(RemoteHardDisable):
|
||||
|
|
@ -772,14 +812,14 @@ func (lp *LoadPoint) Update(sitePower float64) {
|
|||
fallthrough
|
||||
|
||||
case mode == api.ModeOff:
|
||||
err = lp.handler.Ramp(0, true)
|
||||
err = lp.setLimit(0, true)
|
||||
|
||||
case lp.minSocNotReached():
|
||||
err = lp.handler.Ramp(lp.MaxCurrent, true)
|
||||
err = lp.setLimit(lp.MaxCurrent, true)
|
||||
lp.pvDisableTimer() // let PV mode disable immediately afterwards
|
||||
|
||||
case mode == api.ModeNow:
|
||||
err = lp.handler.Ramp(lp.MaxCurrent, true)
|
||||
err = lp.setLimit(lp.MaxCurrent, true)
|
||||
|
||||
case mode == api.ModeMinPV || mode == api.ModePV:
|
||||
targetCurrent := lp.pvMaxCurrent(mode, sitePower)
|
||||
|
|
@ -798,7 +838,7 @@ func (lp *LoadPoint) Update(sitePower float64) {
|
|||
required = true
|
||||
}
|
||||
|
||||
err = lp.handler.Ramp(targetCurrent, required)
|
||||
err = lp.setLimit(targetCurrent, required)
|
||||
}
|
||||
|
||||
// effective disabled status
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
lpMinCurrent int64 = 6
|
||||
lpMaxCurrent int64 = 16
|
||||
minA int64 = 6
|
||||
maxA int64 = 16
|
||||
)
|
||||
|
||||
type Null struct{}
|
||||
|
|
@ -34,6 +34,8 @@ func (n *Null) ChargingTime() (time.Duration, error) {
|
|||
}
|
||||
|
||||
func attachListeners(t *testing.T, lp *LoadPoint) {
|
||||
Voltage = 230 // V
|
||||
|
||||
uiChan := make(chan util.Param)
|
||||
pushChan := make(chan push.Event)
|
||||
lpChan := make(chan *LoadPoint)
|
||||
|
|
@ -58,6 +60,11 @@ 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)
|
||||
}
|
||||
|
||||
lp.Prepare(uiChan, pushChan, lpChan)
|
||||
}
|
||||
|
||||
|
|
@ -67,10 +74,10 @@ func TestNew(t *testing.T) {
|
|||
if lp.Phases != 1 {
|
||||
t.Errorf("Phases %v", lp.Phases)
|
||||
}
|
||||
if lp.MinCurrent != lpMinCurrent {
|
||||
if lp.MinCurrent != minA {
|
||||
t.Errorf("MinCurrent %v", lp.MinCurrent)
|
||||
}
|
||||
if lp.MaxCurrent != lpMaxCurrent {
|
||||
if lp.MaxCurrent != maxA {
|
||||
t.Errorf("MaxCurrent %v", lp.MaxCurrent)
|
||||
}
|
||||
if lp.status != api.StatusNone {
|
||||
|
|
@ -81,60 +88,51 @@ func TestNew(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
func TestUpdatePowerZero(t *testing.T) {
|
||||
tc := []struct {
|
||||
status api.ChargeStatus
|
||||
mode api.ChargeMode
|
||||
expect func(h *mock.MockHandler)
|
||||
expect func(h *mock.MockCharger)
|
||||
}{
|
||||
{api.StatusA, api.ModeOff, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0), false)
|
||||
{api.StatusA, api.ModeOff, func(h *mock.MockCharger) {
|
||||
h.EXPECT().Enable(false)
|
||||
}},
|
||||
{api.StatusA, api.ModeNow, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0), false)
|
||||
{api.StatusA, api.ModeNow, func(h *mock.MockCharger) {
|
||||
h.EXPECT().Enable(false)
|
||||
}},
|
||||
{api.StatusA, api.ModeMinPV, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0), false)
|
||||
{api.StatusA, api.ModeMinPV, func(h *mock.MockCharger) {
|
||||
h.EXPECT().Enable(false)
|
||||
}},
|
||||
{api.StatusA, api.ModePV, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0), false) // zero since update called with 0
|
||||
// h.EXPECT().Enabled().Return(false) // short-circuited due to status != C
|
||||
{api.StatusA, api.ModePV, func(h *mock.MockCharger) {
|
||||
h.EXPECT().Enable(false) // zero since update called with 0
|
||||
}},
|
||||
|
||||
{api.StatusB, api.ModeOff, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0), true)
|
||||
{api.StatusB, api.ModeOff, func(h *mock.MockCharger) {
|
||||
h.EXPECT().Enable(false)
|
||||
}},
|
||||
{api.StatusB, api.ModeNow, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(lpMaxCurrent, true)
|
||||
{api.StatusB, api.ModeNow, func(h *mock.MockCharger) {
|
||||
h.EXPECT().MaxCurrent(maxA) // true
|
||||
}},
|
||||
{api.StatusB, api.ModeMinPV, func(h *mock.MockHandler) {
|
||||
// min since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(lpMinCurrent, false)
|
||||
{api.StatusB, api.ModeMinPV, func(h *mock.MockCharger) {
|
||||
// MaxCurrent omitted since identical value
|
||||
}},
|
||||
{api.StatusB, api.ModePV, func(h *mock.MockHandler) {
|
||||
{api.StatusB, api.ModePV, func(h *mock.MockCharger) {
|
||||
// zero since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(int64(0), false)
|
||||
// h.EXPECT().Enabled().Return(false) // short-circuited due to status != C
|
||||
h.EXPECT().Enable(false)
|
||||
}},
|
||||
|
||||
{api.StatusC, api.ModeOff, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0), true)
|
||||
{api.StatusC, api.ModeOff, func(h *mock.MockCharger) {
|
||||
h.EXPECT().Enable(false)
|
||||
}},
|
||||
{api.StatusC, api.ModeNow, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(lpMaxCurrent, true)
|
||||
{api.StatusC, api.ModeNow, func(h *mock.MockCharger) {
|
||||
h.EXPECT().MaxCurrent(maxA) // true
|
||||
}},
|
||||
{api.StatusC, api.ModeMinPV, func(h *mock.MockHandler) {
|
||||
// min since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(lpMinCurrent, false)
|
||||
{api.StatusC, api.ModeMinPV, func(h *mock.MockCharger) {
|
||||
// MaxCurrent omitted since identical value
|
||||
}},
|
||||
{api.StatusC, api.ModePV, func(h *mock.MockHandler) {
|
||||
// zero since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(int64(0), false)
|
||||
h.EXPECT().Enabled().Return(false)
|
||||
{api.StatusC, api.ModePV, func(h *mock.MockCharger) {
|
||||
// omitted since PV balanced
|
||||
}},
|
||||
}
|
||||
|
||||
|
|
@ -143,43 +141,34 @@ func TestUpdate(t *testing.T) {
|
|||
|
||||
clck := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
handler := mock.NewMockHandler(ctrl)
|
||||
charger := mock.NewMockCharger(ctrl)
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
bus: evbus.New(),
|
||||
clock: clck,
|
||||
charger: charger,
|
||||
chargeMeter: &Null{}, // silence nil panics
|
||||
chargeRater: &Null{}, // silence nil panics
|
||||
chargeTimer: &Null{}, // silence nil panics
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: lpMinCurrent,
|
||||
MaxCurrent: lpMaxCurrent,
|
||||
},
|
||||
handler: handler,
|
||||
status: tc.status, // no status change
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: 1,
|
||||
status: tc.status, // no status change
|
||||
}
|
||||
|
||||
handler.EXPECT().Prepare().Return()
|
||||
attachListeners(t, lp)
|
||||
|
||||
handler.EXPECT().Status().Return(tc.status, nil)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0))
|
||||
|
||||
if tc.status != api.StatusA {
|
||||
handler.EXPECT().Sync()
|
||||
|
||||
if tc.mode == api.ModeMinPV || tc.mode == api.ModePV {
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0))
|
||||
}
|
||||
}
|
||||
// initial status
|
||||
charger.EXPECT().Status().Return(tc.status, nil)
|
||||
charger.EXPECT().Enabled().Return(true, nil)
|
||||
|
||||
if tc.expect != nil {
|
||||
tc.expect(handler)
|
||||
tc.expect(charger)
|
||||
}
|
||||
|
||||
lp.Mode = tc.mode
|
||||
lp.Update(0)
|
||||
lp.Update(0) // sitePower 0
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
|
|
@ -209,7 +198,7 @@ func TestPVHysteresisForStatusC(t *testing.T) {
|
|||
{-6 * 100 * 10, 0, 0},
|
||||
{-6 * 100 * 10, 1, 0},
|
||||
{-6 * 100 * 10, dt - 1, 0},
|
||||
{-6 * 100 * 10, dt + 1, lpMinCurrent},
|
||||
{-6 * 100 * 10, dt + 1, minA},
|
||||
}},
|
||||
// keep disabled when threshold not configured
|
||||
{false, 0, 0, []se{
|
||||
|
|
@ -230,34 +219,34 @@ func TestPVHysteresisForStatusC(t *testing.T) {
|
|||
{-500, 0, 0},
|
||||
{-500, 1, 0},
|
||||
{-500, dt - 1, 0},
|
||||
{-500, dt + 1, lpMinCurrent},
|
||||
{-500, dt + 1, minA},
|
||||
}},
|
||||
// keep enabled at max
|
||||
{true, 500, 0, []se{
|
||||
{-16 * 100 * 10, 0, lpMaxCurrent},
|
||||
{-16 * 100 * 10, 1, lpMaxCurrent},
|
||||
{-16 * 100 * 10, dt - 1, lpMaxCurrent},
|
||||
{-16 * 100 * 10, dt + 1, lpMaxCurrent},
|
||||
{-16 * 100 * 10, 0, maxA},
|
||||
{-16 * 100 * 10, 1, maxA},
|
||||
{-16 * 100 * 10, dt - 1, maxA},
|
||||
{-16 * 100 * 10, dt + 1, maxA},
|
||||
}},
|
||||
// keep enabled at min
|
||||
{true, 500, 0, []se{
|
||||
{-6 * 100 * 10, 0, lpMinCurrent},
|
||||
{-6 * 100 * 10, 1, lpMinCurrent},
|
||||
{-6 * 100 * 10, dt - 1, lpMinCurrent},
|
||||
{-6 * 100 * 10, dt + 1, lpMinCurrent},
|
||||
{-6 * 100 * 10, 0, minA},
|
||||
{-6 * 100 * 10, 1, minA},
|
||||
{-6 * 100 * 10, dt - 1, minA},
|
||||
{-6 * 100 * 10, dt + 1, minA},
|
||||
}},
|
||||
// keep enabled at min (negative threshold)
|
||||
{true, 0, 500, []se{
|
||||
{-500, 0, lpMinCurrent},
|
||||
{-500, 1, lpMinCurrent},
|
||||
{-500, dt - 1, lpMinCurrent},
|
||||
{-500, dt + 1, lpMinCurrent},
|
||||
{-500, 0, minA},
|
||||
{-500, 1, minA},
|
||||
{-500, dt - 1, minA},
|
||||
{-500, dt + 1, minA},
|
||||
}},
|
||||
// disable when threshold met
|
||||
{true, 0, 500, []se{
|
||||
{500, 0, lpMinCurrent},
|
||||
{500, 1, lpMinCurrent},
|
||||
{500, dt - 1, lpMinCurrent},
|
||||
{500, 0, minA},
|
||||
{500, 1, minA},
|
||||
{500, dt - 1, minA},
|
||||
{500, dt + 1, 0},
|
||||
}},
|
||||
// reset enable timer when threshold not met while timer active
|
||||
|
|
@ -267,7 +256,7 @@ func TestPVHysteresisForStatusC(t *testing.T) {
|
|||
{-499, dt - 1, 0}, // should reset timer
|
||||
{-500, dt + 1, 0}, // new begin of timer
|
||||
{-500, 2*dt - 2, 0},
|
||||
{-500, 2*dt - 1, lpMinCurrent},
|
||||
{-500, 2*dt - 1, minA},
|
||||
}},
|
||||
// reset enable timer when threshold not met while timer active and threshold not configured
|
||||
{false, 0, 0, []se{
|
||||
|
|
@ -275,16 +264,16 @@ func TestPVHysteresisForStatusC(t *testing.T) {
|
|||
{-6 * 100 * 10, dt + 1, 0},
|
||||
{-6 * 100 * 10, dt + 2, 0},
|
||||
{-6 * 100 * 10, 2 * dt, 0},
|
||||
{-6 * 100 * 10, 2*dt + 2, lpMinCurrent},
|
||||
{-6 * 100 * 10, 2*dt + 2, minA},
|
||||
}},
|
||||
// reset disable timer when threshold not met while timer active
|
||||
{true, 0, 500, []se{
|
||||
{500, 0, lpMinCurrent},
|
||||
{500, 1, lpMinCurrent},
|
||||
{499, dt - 1, lpMinCurrent}, // reset timer
|
||||
{500, dt + 1, lpMinCurrent}, // within reset timer duration
|
||||
{500, 2*dt - 2, lpMinCurrent}, // still within reset timer duration
|
||||
{500, 2*dt - 1, 0}, // reset timer elapsed
|
||||
{500, 0, minA},
|
||||
{500, 1, minA},
|
||||
{499, dt - 1, minA}, // reset timer
|
||||
{500, dt + 1, minA}, // within reset timer duration
|
||||
{500, 2*dt - 2, minA}, // still within reset timer duration
|
||||
{500, 2*dt - 1, 0}, // reset timer elapsed
|
||||
}},
|
||||
}
|
||||
|
||||
|
|
@ -293,18 +282,16 @@ func TestPVHysteresisForStatusC(t *testing.T) {
|
|||
|
||||
clck := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
handler := mock.NewMockHandler(ctrl)
|
||||
charger := mock.NewMockCharger(ctrl)
|
||||
|
||||
Voltage = 100
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: lpMinCurrent,
|
||||
MaxCurrent: lpMaxCurrent,
|
||||
},
|
||||
handler: handler,
|
||||
Phases: 10,
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
charger: charger,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: 10,
|
||||
Enable: ThresholdConfig{
|
||||
Threshold: tc.enable,
|
||||
Delay: dt,
|
||||
|
|
@ -324,9 +311,9 @@ func TestPVHysteresisForStatusC(t *testing.T) {
|
|||
clck.Set(start.Add(se.delay))
|
||||
|
||||
// maxCurrent will read actual current and enabled state in PV mode
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0))
|
||||
handler.EXPECT().Enabled().Return(tc.enabled)
|
||||
// charger.EXPECT().Enabled().Return(tc.enabled, nil)
|
||||
|
||||
lp.enabled = tc.enabled
|
||||
current := lp.pvMaxCurrent(api.ModePV, se.site)
|
||||
|
||||
if current != se.current {
|
||||
|
|
@ -341,25 +328,20 @@ func TestPVHysteresisForStatusC(t *testing.T) {
|
|||
func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
|
||||
clck := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
handler := mock.NewMockHandler(ctrl)
|
||||
|
||||
Voltage = 100
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: lpMinCurrent,
|
||||
MaxCurrent: lpMaxCurrent,
|
||||
},
|
||||
handler: handler,
|
||||
Phases: 10,
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
Phases: 10,
|
||||
}
|
||||
|
||||
// not connected, test PV mode logic short-circuited
|
||||
lp.status = api.StatusA
|
||||
|
||||
// maxCurrent will read actual current in PV mode
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0))
|
||||
|
||||
// maxCurrent will read enabled state in PV mode
|
||||
sitePower := -float64(minA*lp.Phases)*Voltage + 1 // 1W below min power
|
||||
|
|
@ -375,7 +357,7 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
|
|||
func TestDisableAndEnableAtTargetSoC(t *testing.T) {
|
||||
clock := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
handler := mock.NewMockHandler(ctrl)
|
||||
charger := mock.NewMockCharger(ctrl)
|
||||
vehicle := mock.NewMockVehicle(ctrl)
|
||||
|
||||
// wrap vehicle with estimator
|
||||
|
|
@ -383,17 +365,15 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
|
|||
socEstimator := wrapper.NewSocEstimator(util.NewLogger("foo"), vehicle, false)
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
bus: evbus.New(),
|
||||
clock: clock,
|
||||
chargeMeter: &Null{}, // silence nil panics
|
||||
chargeRater: &Null{}, // silence nil panics
|
||||
chargeTimer: &Null{}, // silence nil panics
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: lpMinCurrent,
|
||||
MaxCurrent: lpMaxCurrent,
|
||||
},
|
||||
handler: handler,
|
||||
log: util.NewLogger("foo"),
|
||||
bus: evbus.New(),
|
||||
clock: clock,
|
||||
charger: charger,
|
||||
chargeMeter: &Null{}, // silence nil panics
|
||||
chargeRater: &Null{}, // silence nil panics
|
||||
chargeTimer: &Null{}, // silence nil panics
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
vehicle: vehicle, // needed for targetSoC check
|
||||
socEstimator: socEstimator, // instead of vehicle: vehicle,
|
||||
status: api.StatusC,
|
||||
|
|
@ -403,43 +383,39 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
handler.EXPECT().Prepare().Return()
|
||||
attachListeners(t, lp)
|
||||
|
||||
// charging below target
|
||||
handler.EXPECT().TargetCurrent().Return(int64(6))
|
||||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.enabled = true
|
||||
lp.maxCurrent = minA
|
||||
|
||||
t.Log("charging below target")
|
||||
vehicle.EXPECT().ChargeState().Return(85.0, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().MaxCurrent(maxA).Return(nil)
|
||||
lp.Update(500)
|
||||
|
||||
// charging above target deactivates charger
|
||||
t.Log("charging above target deactivates charger")
|
||||
clock.Add(5 * time.Minute)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(16))
|
||||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
vehicle.EXPECT().ChargeState().Return(90.0, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().Ramp(int64(0), true).Return(nil) // true due to immediately handling climate requests
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Enable(false).Return(nil)
|
||||
lp.Update(500)
|
||||
|
||||
// deactivated charger changes status to B
|
||||
t.Log("deactivated charger changes status to B")
|
||||
clock.Add(5 * time.Minute)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0))
|
||||
handler.EXPECT().Status().Return(api.StatusB, nil)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
vehicle.EXPECT().ChargeState().Return(95.0, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().Ramp(int64(0), true).Return(nil) // true due to immediately handling climate requests
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
lp.Update(-5000)
|
||||
|
||||
// soc has fallen below target
|
||||
t.Log("soc has fallen below target")
|
||||
clock.Add(5 * time.Minute)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0))
|
||||
handler.EXPECT().Status().Return(api.StatusB, nil)
|
||||
vehicle.EXPECT().ChargeState().Return(85.0, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil) // TODO don't treat this as forced change
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Enable(true).Return(nil)
|
||||
lp.Update(-5000)
|
||||
|
||||
ctrl.Finish()
|
||||
|
|
@ -448,21 +424,19 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
|
|||
func TestSetModeAndSocAtDisconnect(t *testing.T) {
|
||||
clock := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
handler := mock.NewMockHandler(ctrl)
|
||||
charger := mock.NewMockCharger(ctrl)
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
bus: evbus.New(),
|
||||
clock: clock,
|
||||
charger: charger,
|
||||
chargeMeter: &Null{}, // silence nil panics
|
||||
chargeRater: &Null{}, // silence nil panics
|
||||
chargeTimer: &Null{}, // silence nil panics
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: lpMinCurrent,
|
||||
MaxCurrent: lpMaxCurrent,
|
||||
},
|
||||
handler: handler,
|
||||
status: api.StatusC,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
status: api.StatusC,
|
||||
OnDisconnect: struct {
|
||||
Mode api.ChargeMode `mapstructure:"mode"` // Charge mode to apply when car disconnected
|
||||
TargetSoC int `mapstructure:"targetSoC"` // Target SoC to apply when car disconnected
|
||||
|
|
@ -472,21 +446,23 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
handler.EXPECT().Prepare().Return()
|
||||
attachListeners(t, lp)
|
||||
|
||||
lp.enabled = true
|
||||
lp.maxCurrent = minA
|
||||
lp.Mode = api.ModeNow
|
||||
handler.EXPECT().TargetCurrent().Return(int64(6))
|
||||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
|
||||
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)
|
||||
lp.Update(500)
|
||||
|
||||
t.Log("switch off when disconnected")
|
||||
clock.Add(5 * time.Minute)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(16))
|
||||
handler.EXPECT().Status().Return(api.StatusA, nil)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
handler.EXPECT().Ramp(int64(0), false).Return(nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusA, nil)
|
||||
charger.EXPECT().Enable(false).Return(nil)
|
||||
lp.Update(-3000)
|
||||
|
||||
if lp.Mode != api.ModeOff {
|
||||
|
|
@ -519,91 +495,74 @@ func cacheExpecter(t *testing.T, lp *LoadPoint) (*util.Cache, func(key string, v
|
|||
func TestChargedEnergyAtDisconnect(t *testing.T) {
|
||||
clock := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
handler := mock.NewMockHandler(ctrl)
|
||||
charger := mock.NewMockCharger(ctrl)
|
||||
rater := mock.NewMockChargeRater(ctrl)
|
||||
|
||||
lp := &LoadPoint{
|
||||
log: util.NewLogger("foo"),
|
||||
bus: evbus.New(),
|
||||
clock: clock,
|
||||
charger: charger,
|
||||
chargeMeter: &Null{}, // silence nil panics
|
||||
chargeRater: rater,
|
||||
chargeTimer: &Null{}, // silence nil panics
|
||||
HandlerConfig: HandlerConfig{
|
||||
MinCurrent: lpMinCurrent,
|
||||
MaxCurrent: lpMaxCurrent,
|
||||
},
|
||||
handler: handler,
|
||||
status: api.StatusC,
|
||||
MinCurrent: minA,
|
||||
MaxCurrent: maxA,
|
||||
status: api.StatusC,
|
||||
}
|
||||
|
||||
lp.Mode = api.ModeNow
|
||||
handler.EXPECT().Prepare().Return()
|
||||
attachListeners(t, lp)
|
||||
|
||||
lp.enabled = true
|
||||
lp.maxCurrent = maxA
|
||||
lp.Mode = api.ModeNow
|
||||
|
||||
// attach cache for verifying values
|
||||
_, expectCache := cacheExpecter(t, lp)
|
||||
|
||||
// start charging at 0 kWh
|
||||
handler.EXPECT().TargetCurrent().Return(int64(6))
|
||||
t.Log("start charging at 0 kWh")
|
||||
rater.EXPECT().ChargedEnergy().Return(0.0, nil)
|
||||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1)
|
||||
|
||||
// at 1:00h charging at 5 kWh
|
||||
t.Log("at 1:00h charging at 5 kWh")
|
||||
clock.Add(time.Hour)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(16))
|
||||
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
|
||||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
// handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
// at 1:00h stop charging at 5 kWh
|
||||
t.Log("at 1:00h stop charging at 5 kWh")
|
||||
clock.Add(time.Second)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(16))
|
||||
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
|
||||
handler.EXPECT().Status().Return(api.StatusB, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
lp.Update(-1)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
// at 1:00h restart charging at 5 kWh
|
||||
t.Log("at 1:00h restart charging at 5 kWh")
|
||||
clock.Add(time.Second)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(16))
|
||||
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
|
||||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
// at 1:30h continue charging at 7.5 kWh
|
||||
t.Log("at 1:30h continue charging at 7.5 kWh")
|
||||
clock.Add(30 * time.Minute)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(16))
|
||||
rater.EXPECT().ChargedEnergy().Return(7.5, nil)
|
||||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
// handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1)
|
||||
expectCache("chargedEnergy", 7500.0)
|
||||
|
||||
// at 2:00h stop charging at 10 kWh
|
||||
t.Log("at 2:00h stop charging at 10 kWh")
|
||||
clock.Add(30 * time.Minute)
|
||||
handler.EXPECT().TargetCurrent().Return(int64(16))
|
||||
rater.EXPECT().ChargedEnergy().Return(10.0, nil)
|
||||
handler.EXPECT().Status().Return(api.StatusB, nil)
|
||||
handler.EXPECT().Sync().Return()
|
||||
handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
handler.EXPECT().Ramp(int64(16), true).Return(nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
lp.Update(-1)
|
||||
expectCache("chargedEnergy", 10000.0)
|
||||
|
||||
|
|
|
|||
|
|
@ -150,11 +150,10 @@ func (site *Site) DumpConfig() {
|
|||
|
||||
lp.log.INFO.Printf(" mode: %s", lp.GetMode())
|
||||
|
||||
charger := lp.handler.(*ChargerHandler).charger
|
||||
_, power := charger.(api.Meter)
|
||||
_, energy := charger.(api.MeterEnergy)
|
||||
_, currents := charger.(api.MeterCurrent)
|
||||
_, timer := charger.(api.ChargeTimer)
|
||||
_, power := lp.charger.(api.Meter)
|
||||
_, energy := lp.charger.(api.MeterEnergy)
|
||||
_, currents := lp.charger.(api.MeterCurrent)
|
||||
_, timer := lp.charger.(api.ChargeTimer)
|
||||
|
||||
lp.log.INFO.Printf(" charger: power %s energy %s currents %s timer %s",
|
||||
presence[power],
|
||||
|
|
|
|||
|
|
@ -1,115 +0,0 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/andig/evcc/core (interfaces: Handler)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
api "github.com/andig/evcc/api"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockHandler is a mock of Handler interface
|
||||
type MockHandler struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockHandlerMockRecorder
|
||||
}
|
||||
|
||||
// MockHandlerMockRecorder is the mock recorder for MockHandler
|
||||
type MockHandlerMockRecorder struct {
|
||||
mock *MockHandler
|
||||
}
|
||||
|
||||
// NewMockHandler creates a new mock instance
|
||||
func NewMockHandler(ctrl *gomock.Controller) *MockHandler {
|
||||
mock := &MockHandler{ctrl: ctrl}
|
||||
mock.recorder = &MockHandlerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockHandler) EXPECT() *MockHandlerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Enabled mocks base method
|
||||
func (m *MockHandler) Enabled() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Enabled")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Enabled indicates an expected call of Enabled
|
||||
func (mr *MockHandlerMockRecorder) Enabled() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enabled", reflect.TypeOf((*MockHandler)(nil).Enabled))
|
||||
}
|
||||
|
||||
// Prepare mocks base method
|
||||
func (m *MockHandler) Prepare() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Prepare")
|
||||
}
|
||||
|
||||
// Prepare indicates an expected call of Prepare
|
||||
func (mr *MockHandlerMockRecorder) Prepare() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockHandler)(nil).Prepare))
|
||||
}
|
||||
|
||||
// Ramp mocks base method
|
||||
func (m *MockHandler) Ramp(arg0 int64, arg1 bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Ramp", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Ramp indicates an expected call of Ramp
|
||||
func (mr *MockHandlerMockRecorder) Ramp(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ramp", reflect.TypeOf((*MockHandler)(nil).Ramp), arg0, arg1)
|
||||
}
|
||||
|
||||
// Status mocks base method
|
||||
func (m *MockHandler) Status() (api.ChargeStatus, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Status")
|
||||
ret0, _ := ret[0].(api.ChargeStatus)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Status indicates an expected call of Status
|
||||
func (mr *MockHandlerMockRecorder) Status() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockHandler)(nil).Status))
|
||||
}
|
||||
|
||||
// Sync mocks base method
|
||||
func (m *MockHandler) Sync() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Sync")
|
||||
}
|
||||
|
||||
// Sync indicates an expected call of Sync
|
||||
func (mr *MockHandlerMockRecorder) Sync() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sync", reflect.TypeOf((*MockHandler)(nil).Sync))
|
||||
}
|
||||
|
||||
// TargetCurrent mocks base method
|
||||
func (m *MockHandler) TargetCurrent() int64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "TargetCurrent")
|
||||
ret0, _ := ret[0].(int64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// TargetCurrent indicates an expected call of TargetCurrent
|
||||
func (mr *MockHandlerMockRecorder) TargetCurrent() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TargetCurrent", reflect.TypeOf((*MockHandler)(nil).TargetCurrent))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue