Repository: andig/evcc
Pull request #482 (2020-11-26 15:44:29)

Refactor charger handler part 2


Patch:
diff --git a/core/chargerhandler.go b/core/chargerhandler.go
deleted file mode 100644
index 73aa0e5b..00000000
--- a/core/chargerhandler.go
+++ /dev/null
@@ -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
-}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 78b99a65..bd825faf 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -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
diff --git a/core/site.go b/core/site.go
index 78363622..30e09cce 100644
--- a/core/site.go
+++ b/core/site.go
@@ -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],
diff --git a/mock/mock_chargerhandler.go b/mock/mock_chargerhandler.go
deleted file mode 100644
index ad86552b..00000000
--- a/mock/mock_chargerhandler.go
+++ /dev/null
@@ -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))
-}


Test patch:
diff --git a/core/chargerhandler_test.go b/core/chargerhandler_test.go
deleted file mode 100644
index 316707ea..00000000
--- a/core/chargerhandler_test.go
+++ /dev/null
@@ -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()
-	}
-}
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 76630622..004d1c50 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -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)