Repository: evcc-io/evcc
Pull request #11311 (2023-12-28 15:09:52)

Replace current precendence with min/max logic
Fix https://github.com/evcc-io/evcc/issues/11280


TODO

- [x] check UI min/max dialog
- [x] allow charger to override loadpoint `minCurrent` (EEbus/ISO)
- [x] add tests

Out of scope

- [ ] limit UI dialog to loadpoint min/max @naltatis 


Patch:
diff --git a/api/api.go b/api/api.go
index 8568b12c..7cdcba1e 100644
--- a/api/api.go
+++ b/api/api.go
@@ -9,7 +9,7 @@ import (
 	"time"
 )
 
-//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
+//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
 
 // ChargeMode is the charge operation mode. Valid values are off, now, minpv and pv
 type ChargeMode string
diff --git a/api/mock.go b/api/mock.go
index fbab5af0..0fb7eb35 100644
--- a/api/mock.go
+++ b/api/mock.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController)
+// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController)
 
 // Package api is a generated GoMock package.
 package api
@@ -129,6 +129,45 @@ func (mr *MockChargeStateMockRecorder) Status() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockChargeState)(nil).Status))
 }
 
+// MockCurrentLimiter is a mock of CurrentLimiter interface.
+type MockCurrentLimiter struct {
+	ctrl     *gomock.Controller
+	recorder *MockCurrentLimiterMockRecorder
+}
+
+// MockCurrentLimiterMockRecorder is the mock recorder for MockCurrentLimiter.
+type MockCurrentLimiterMockRecorder struct {
+	mock *MockCurrentLimiter
+}
+
+// NewMockCurrentLimiter creates a new mock instance.
+func NewMockCurrentLimiter(ctrl *gomock.Controller) *MockCurrentLimiter {
+	mock := &MockCurrentLimiter{ctrl: ctrl}
+	mock.recorder = &MockCurrentLimiterMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCurrentLimiter) EXPECT() *MockCurrentLimiterMockRecorder {
+	return m.recorder
+}
+
+// GetMinMaxCurrent mocks base method.
+func (m *MockCurrentLimiter) GetMinMaxCurrent() (float64, float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMinMaxCurrent")
+	ret0, _ := ret[0].(float64)
+	ret1, _ := ret[1].(float64)
+	ret2, _ := ret[2].(error)
+	return ret0, ret1, ret2
+}
+
+// GetMinMaxCurrent indicates an expected call of GetMinMaxCurrent.
+func (mr *MockCurrentLimiterMockRecorder) GetMinMaxCurrent() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinMaxCurrent", reflect.TypeOf((*MockCurrentLimiter)(nil).GetMinMaxCurrent))
+}
+
 // MockPhaseSwitcher is a mock of PhaseSwitcher interface.
 type MockPhaseSwitcher struct {
 	ctrl     *gomock.Controller
diff --git a/charger/delta.go b/charger/delta.go
new file mode 100644
index 00000000..cb971510
--- /dev/null
+++ b/charger/delta.go
@@ -0,0 +1,291 @@
+package charger
+
+import (
+	"fmt"
+	"math"
+	"sync"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/core/loadpoint"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/evcc-io/evcc/util/sponsor"
+	"github.com/volkszaehler/mbmd/encoding"
+)
+
+// Delta charger implementation
+type Delta struct {
+	log     *util.Logger
+	conn    *modbus.Connection
+	lp      loadpoint.API
+	mu      sync.Mutex
+	curr    float64
+	base    uint16
+	enabled bool
+}
+
+const (
+	// EV Charger
+	// Read Input Registers (0x04)
+	deltaRegState   = 100 // Charger State - UINT16 0: not ready, 1: operational, 10: faulted, 255: not responding
+	deltaRegVersion = 101 // Charger Version - UINT16
+	deltaRegCount   = 102 // Charger EVSE Count - UINT16
+	deltaRegError   = 103 // Charger Error - UINT16
+	deltaRegSerial  = 110 // Charger Serial - STRING20
+	deltaRegModel   = 130 // Charger Model - STRING20
+
+	// Write Multiple Registers (0x10)
+	deltaRegCommunicationTimeoutEnabled = 201 // Communication Timeout Enabled 0/1
+	deltaRegCommunicationTimeout        = 202 // Communication Timeout [s]
+	deltaRegFallbackPower               = 203 // Fallback Power [W]
+
+	// EVSE - The following Register tables are defined as repeating blocks for each single EVSE
+	// Read Input Registers (0x04)
+	deltaRegEvseState                 = 0   // EVSE State - UINT16 0: Unavailable, 1: Available, 2: Occupied, 3: Preparing, 4: Charging, 5: Finishing, 6: Suspended EV, 7: Suspended EVSE, 8: Not ready, 9: Faulted
+	deltaRegEvseChargerState          = 1   // EVSE Charger State - UINT16 0: Charging process not started (no vehicle connected), 1: Connected, waiting for release (by RFID or local), 2: Charging process starts, 3: Charging, 4: Suspended (paused), 5: Charging process successfully completed (vehicle still plugged in), 6: Charging process completed by user (vehicle still plugged in), 7: Charging ended with error (vehicle still connected)
+	deltaRegEvseActualOutputVoltage   = 3   // EVSE Actual Output Voltage - FLOAT32 [V]
+	deltaRegEvseActualChargingPower   = 5   // EVSE Actual Charging Power - UINT32 [W]
+	deltaRegEvseActualChargingCurrent = 7   // EVSE Actual Charging Current - FLOAT32 [A]
+	deltaRegEvseActualOutputPower     = 9   // EVSE Actual Output Power - FLOAT32 [W]
+	deltaRegEvseSoc                   = 11  // EVSE SOC [%/10]
+	deltaRegEvseChargingTime          = 17  // EVSE Charging Time [s]
+	deltaRegEvseChargedEnergy         = 19  // EVSE Charged Energy [Wh]
+	deltaRegEvseRfidUID               = 100 // EVSE Used Authentication ID - STRING
+
+	// Write Multiple Registers (0x10)
+	deltaRegEvseChargingPowerLimit = 600 // EVSE Charging Power Limit - UINT32 [W]
+	deltaRegEvseSuspendCharging    = 602 // EVSE Suspend Charging - UINT16 - 0: no pause, 1 charging pause (lock on)
+)
+
+func init() {
+	registry.Add("delta", NewDeltaFromConfig)
+}
+
+// NewDeltaFromConfig creates a Delta charger from generic config
+func NewDeltaFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := struct {
+		Connector       uint16
+		modbus.Settings `mapstructure:",squash"`
+	}{
+		Connector: 1,
+		Settings: modbus.Settings{
+			ID: 1,
+		},
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewDelta(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID, cc.Connector)
+}
+
+// NewDelta creates Delta charger
+func NewDelta(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, connector uint16) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	log := util.NewLogger("delta")
+	conn.Logger(log.TRACE)
+
+	wb := &Delta{
+		log:  log,
+		conn: conn,
+		curr: 6000, // assume min current
+	}
+
+	wb.base = connector * 1000
+
+	// get failsafe timeout from charger
+	b, err := wb.conn.ReadHoldingRegisters(deltaRegCommunicationTimeout, 1)
+	if err != nil {
+		return nil, fmt.Errorf("failsafe timeout: %w", err)
+	}
+	if u := encoding.Uint16(b); u > 0 {
+		go wb.heartbeat(time.Duration(u) * time.Second / 2)
+	}
+
+	return wb, err
+}
+
+func (wb *Delta) heartbeat(timeout time.Duration) {
+	for range time.Tick(timeout) {
+		wb.mu.Lock()
+		var curr float64
+		if wb.enabled {
+			curr = wb.curr
+		}
+		wb.mu.Unlock()
+		if err := wb.setCurrent(curr); err != nil {
+			wb.log.ERROR.Println("heartbeat:", err)
+		}
+	}
+}
+
+// Status implements the api.Charger interface
+func (wb *Delta) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseChargerState, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	// 0: Charging process not started (no vehicle connected)
+	// 1: Connected, waiting for release (by RFID or local)
+	// 2: Charging process starts
+	// 3: Charging
+	// 4: Suspended (loading paused)
+	// 5: Charging process successfully completed (vehicle still plugged in)
+	// 6: Charging process completed by user (vehicle still plugged in)
+	// 7: Charging ended with error (vehicle still connected)
+	switch s := encoding.Uint16(b); s {
+	case 0:
+		return api.StatusA, nil
+	case 1, 2, 4, 5, 6, 7:
+		return api.StatusB, nil
+	case 3:
+		return api.StatusC, nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %0x", s)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Delta) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wb.base+deltaRegEvseChargingPowerLimit, 2)
+	if err != nil {
+		return false, err
+	}
+
+	return encoding.Uint32(b) > 0, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *Delta) Enable(enable bool) error {
+	var curr float64
+	if enable {
+		wb.mu.Lock()
+		curr = wb.curr
+		wb.mu.Unlock()
+	}
+
+	err := wb.setCurrent(curr)
+	if err == nil {
+		wb.mu.Lock()
+		wb.enabled = enable
+		wb.mu.Unlock()
+	}
+
+	return err
+}
+
+// setCurrent writes the current limit in A
+func (wb *Delta) setCurrent(current float64) error {
+	activePhases := 3
+	if wb.lp != nil {
+		activePhases = wb.lp.ActivePhases()
+	}
+
+	b := make([]byte, 4)
+	encoding.PutUint32(b, uint32(math.Trunc(230.0*current*float64(activePhases))))
+
+	_, err := wb.conn.WriteMultipleRegisters(wb.base+deltaRegEvseChargingPowerLimit, 2, b)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Delta) MaxCurrent(current int64) error {
+	return wb.MaxCurrentMillis(float64(current))
+}
+
+var _ api.ChargerEx = (*Delta)(nil)
+
+// MaxCurrentMillis implements the api.ChargerEx interface
+func (wb *Delta) MaxCurrentMillis(current float64) error {
+	if current < 6 {
+		return fmt.Errorf("invalid current %.1f", current)
+	}
+
+	err := wb.setCurrent(current)
+	if err == nil {
+		wb.mu.Lock()
+		wb.curr = current
+		wb.mu.Unlock()
+	}
+
+	return err
+}
+
+var _ api.Meter = (*Delta)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *Delta) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseActualChargingPower, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(encoding.Uint32(b)), err
+}
+
+var _ api.ChargeRater = (*Delta)(nil)
+
+// ChargedEnergy implements the api.ChargeRater interface
+func (wb *Delta) ChargedEnergy() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseChargedEnergy, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(encoding.Uint32(b)) / 1e3, err
+}
+
+var _ api.Identifier = (*Delta)(nil)
+
+// Identify implements the api.Identifier interface
+func (wb *Delta) Identify() (string, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseRfidUID, 6)
+	if err != nil {
+		return "", err
+	}
+
+	return bytesAsString(b), nil
+}
+
+var _ api.Diagnosis = (*Delta)(nil)
+
+// Diagnose implements the api.Diagnosis interface
+func (wb *Delta) Diagnose() {
+	if b, err := wb.conn.ReadInputRegisters(deltaRegState, 1); err == nil {
+		fmt.Printf("\tState:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegVersion, 1); err == nil {
+		fmt.Printf("\tVersion:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegCount, 1); err == nil {
+		fmt.Printf("\tEVSE Count:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegError, 1); err == nil {
+		fmt.Printf("\tError:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegSerial, 20); err == nil {
+		fmt.Printf("\tSerial:\t%s\n", bytesAsString(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegModel, 20); err == nil {
+		fmt.Printf("\tModel:\t%s\n", bytesAsString(b))
+	}
+}
+
+var _ loadpoint.Controller = (*Delta)(nil)
+
+// LoadpointControl implements loadpoint.Controller
+func (wb *Delta) LoadpointControl(lp loadpoint.API) {
+	wb.lp = lp
+}
diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go
index 276947b6..57a441f6 100644
--- a/core/loadpoint_effective.go
+++ b/core/loadpoint_effective.go
@@ -60,38 +60,46 @@ func (lp *Loadpoint) SocBasedPlanning() bool {
 
 // effectiveMinCurrent returns the effective min current
 func (lp *Loadpoint) effectiveMinCurrent() float64 {
+	minCurrent := lp.GetMinCurrent()
+
 	if v := lp.GetVehicle(); v != nil {
 		if res, ok := v.OnIdentified().GetMinCurrent(); ok {
-			return res
+			minCurrent = max(minCurrent, res)
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
 		if res, _, err := c.GetMinMaxCurrent(); err == nil {
-			lp.publish(keys.EffectiveMinCurrent, res)
-			return res
+			if res > 0 && res < minCurrent {
+				minCurrent = res
+			} else {
+				minCurrent = max(minCurrent, res)
+			}
+			lp.publish(keys.EffectiveMinCurrent, minCurrent)
 		}
 	}
 
-	return lp.GetMinCurrent()
+	return minCurrent
 }
 
 // effectiveMaxCurrent returns the effective max current
 func (lp *Loadpoint) effectiveMaxCurrent() float64 {
+	maxCurrent := lp.GetMaxCurrent()
+
 	if v := lp.GetVehicle(); v != nil {
 		if res, ok := v.OnIdentified().GetMaxCurrent(); ok {
-			return res
+			maxCurrent = min(maxCurrent, res)
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
 		if _, res, err := c.GetMinMaxCurrent(); err == nil {
-			lp.publish(keys.EffectiveMaxCurrent, res)
-			return res
+			maxCurrent = min(maxCurrent, res)
+			lp.publish(keys.EffectiveMaxCurrent, maxCurrent)
 		}
 	}
 
-	return lp.GetMaxCurrent()
+	return maxCurrent
 }
 
 // effectiveLimitSoc returns the effective session limit soc
diff --git a/templates/definition/charger/delta.yaml b/templates/definition/charger/delta.yaml
new file mode 100644
index 00000000..c296be49
--- /dev/null
+++ b/templates/definition/charger/delta.yaml
@@ -0,0 +1,26 @@
+template: delta
+products:
+  - brand: Delta
+    description:
+      generic: AC Max Basic
+  - brand: Delta
+    description:
+      generic: AC MAX Smart
+  - brand: Delta
+    description:
+      generic: SLIM Charger
+  - brand: Delta
+    description:
+      generic: Ultra Fast Charger
+capabilities: ["mA", "rfid"]
+requirements:
+  evcc: ["sponsorship"]
+params:
+  - name: modbus
+    choice: ["rs485", "tcpip"]
+    baudrate: 115200
+  - name: connector
+render: |
+  type: delta
+  {{- include "modbus" . }}
+  connector: {{ .connector }}
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
deleted file mode 100644
index 06ab4e69..00000000
--- a/templates/evcc.io/brands.json
+++ /dev/null
@@ -1,237 +0,0 @@
-{
- "Chargers": [
-  "ABB",
-  "ABL",
-  "Alfen",
-  "Alphatec",
-  "Amperfied",
-  "Audi",
-  "Bender",
-  "BMW",
-  "cFos",
-  "Compleo",
-  "Cupra",
-  "Dadapower",
-  "DaheimLaden",
-  "E.ON Drive",
-  "E3/DC",
-  "Easee",
-  "Ebee",
-  "echarge",
-  "Elli",
-  "EM2GO",
-  "Ensto",
-  "ESL",
-  "Etrel",
-  "EVBox",
-  "Fronius",
-  "Garo",
-  "go-e",
-  "HardyBarth",
-  "Heidelberg",
-  "Hesotec",
-  "Homecharge",
-  "Huawei",
-  "Innogy",
-  "INRO",
-  "Juice",
-  "KEBA",
-  "KSE",
-  "LadeFoxx",
-  "Mennekes",
-  "NRGKick",
-  "OBO Bettermann",
-  "OpenEVSE",
-  "openWB",
-  "Optec",
-  "Orbis",
-  "PC Electric",
-  "Phoenix Contact",
-  "Porsche",
-  "Pracht",
-  "Pulsares",
-  "Schneider",
-  "Schrack",
-  "SENEC",
-  "Siemens",
-  "Skoda",
-  "SMA",
-  "SolarEdge",
-  "Sonnen",
-  "Stark in Strom",
-  "TechniSat",
-  "Tesla",
-  "TinkerForge",
-  "Ubitricity",
-  "Vestel",
-  "Victron",
-  "Volkswagen",
-  "Wallbe",
-  "wallbox",
-  "Walther Werke",
-  "Webasto",
-  "Zaptec"
- ],
- "SmartPlugs": [
-  "AVM",
-  "Homematic IP",
-  "HomeWizard",
-  "myStrom",
-  "Shelly",
-  "Tasmota",
-  "TP-Link"
- ],
- "Meters": [
-  "ABB",
-  "Acrel",
-  "Alpha ESS",
-  "AVM",
-  "Bernecker Engineering",
-  "Carlo Gavazzi",
-  "cFos",
-  "Deye",
-  "DSMR",
-  "DZG",
-  "E3/DC",
-  "Eastron",
-  "Enphase",
-  "ESPHome",
-  "FENECON",
-  "FoxESS",
-  "Fronius",
-  "go-e",
-  "GoodWe",
-  "Growatt",
-  "Homematic IP",
-  "HomeWizard",
-  "Huawei",
-  "inepro",
-  "Janitza",
-  "KEBA",
-  "Kostal",
-  "LG",
-  "Loxone",
-  "M-TEC",
-  "my-PV",
-  "myStrom",
-  "OpenEMS",
-  "Orno",
-  "P1Monitor",
-  "Powerfox",
-  "Qcells",
-  "RCT",
-  "Saia-Burgess Controls",
-  "SAJ",
-  "SAX",
-  "Schneider Electric",
-  "SENEC",
-  "Shelly",
-  "Siemens",
-  "SMA",
-  "Smartfox",
-  "SofarSolar",
-  "Solaranzeige",
-  "SolarEdge",
-  "SolarMax",
-  "Solarwatt",
-  "Solax",
-  "Sonnen",
-  "Sungrow",
-  "Sunsynk",
-  "Tasmota",
-  "Tesla",
-  "Tibber",
-  "TQ",
-  "VARTA",
-  "Victron",
-  "Volkszähler",
-  "Youless",
-  "ZCS Azzurro",
-  "Zuidwijk"
- ],
- "PVBattery": [
-  "Acrel",
-  "Ads-tec",
-  "Alpha ESS",
-  "Anker",
-  "AVM",
-  "Bosswerk",
-  "Deye",
-  "E3/DC",
-  "Enphase",
-  "FENECON",
-  "FoxESS",
-  "Fronius",
-  "Ginlong",
-  "go-e",
-  "GoodWe",
-  "Growatt",
-  "Homematic IP",
-  "HomeWizard",
-  "Hoymiles",
-  "Huawei",
-  "IGEN Tech",
-  "Kostal",
-  "LG",
-  "Loxone",
-  "M-TEC",
-  "myStrom",
-  "OpenEMS",
-  "Powerfox",
-  "Qcells",
-  "RCT",
-  "SAJ",
-  "SAX",
-  "SENEC",
-  "Senergy",
-  "Shelly",
-  "SMA",
-  "Smartfox",
-  "SofarSolar",
-  "Solaranzeige",
-  "SolarEdge",
-  "SolarMax",
-  "Solarwatt",
-  "Solax",
-  "Sonnen",
-  "Steca",
-  "Sungrow",
-  "Sunsynk",
-  "Tasmota",
-  "Tesla",
-  "TP-Link",
-  "VARTA",
-  "Victron",
-  "Youless",
-  "ZCS Azzurro"
- ],
- "Vehicles": [
-  "Aiways",
-  "Audi",
-  "BMW",
-  "Citroën",
-  "Dacia",
-  "DS",
-  "Fiat",
-  "Ford",
-  "Hyundai",
-  "Jaguar",
-  "Jeep",
-  "Kia",
-  "Land Rover",
-  "Mini",
-  "Nissan",
-  "NIU",
-  "Opel",
-  "Peugeot",
-  "Polestar",
-  "Porsche",
-  "Renault",
-  "Seat",
-  "Skoda",
-  "Smart",
-  "Tesla",
-  "Volkswagen",
-  "Volvo"
- ]
-}
\ No newline at end of file


Test patch:
diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go
index 888ebca2..060a836f 100644
--- a/core/loadpoint_effective_test.go
+++ b/core/loadpoint_effective_test.go
@@ -3,7 +3,9 @@ package core
 import (
 	"testing"
 
+	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
+	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -11,3 +13,54 @@ func TestEffectiveLimitSoc(t *testing.T) {
 	lp := NewLoadpoint(util.NewLogger("foo"), nil)
 	assert.Equal(t, 100, lp.effectiveLimitSoc())
 }
+
+func TestEffectiveMinMaxCurrent(t *testing.T) {
+	tc := []struct {
+		chargerMin, chargerMax     float64
+		vehicleMin, vehicleMax     float64
+		effectiveMin, effectiveMax float64
+	}{
+		{0, 0, 0, 0, 6, 16},
+		{1, 10, 0, 0, 1, 10},     // charger lower
+		{10, 20, 0, 0, 10, 16},   // charger higher - max ignored
+		{0, 0, 1, 10, 6, 10},     // vehicle lower - min ignored
+		{0, 0, 10, 20, 10, 16},   // vehicle higher - max ignored
+		{1, 10, 2, 12, 1, 10},    // charger + vehicle lower
+		{10, 20, 12, 22, 10, 16}, // charger + vehicle higher
+	}
+
+	for _, tc := range tc {
+		t.Logf("%+v", tc)
+		ctrl := gomock.NewController(t)
+
+		lp := NewLoadpoint(util.NewLogger("foo"), nil)
+		lp.charger = api.NewMockCharger(ctrl)
+
+		if tc.chargerMin+tc.chargerMax > 0 {
+			currentLimiter := api.NewMockCurrentLimiter(ctrl)
+			currentLimiter.EXPECT().GetMinMaxCurrent().Return(tc.chargerMin, tc.chargerMax, nil).AnyTimes()
+
+			lp.charger = struct {
+				api.Charger
+				api.CurrentLimiter
+			}{
+				Charger:        lp.charger,
+				CurrentLimiter: currentLimiter,
+			}
+		}
+
+		if tc.vehicleMin+tc.vehicleMax > 0 {
+			vehicle := api.NewMockVehicle(ctrl)
+			ac := api.ActionConfig{
+				MinCurrent: tc.vehicleMin,
+				MaxCurrent: tc.vehicleMax,
+			}
+			vehicle.EXPECT().OnIdentified().Return(ac).AnyTimes()
+
+			lp.vehicle = vehicle
+		}
+
+		assert.Equal(t, tc.effectiveMin, lp.effectiveMinCurrent())
+		assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent())
+	}
+}
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 89330116..a08ab6c4 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -88,9 +88,6 @@ func TestNew(t *testing.T) {
 	if lp.phases != 0 {
 		t.Errorf("Phases %v", lp.phases)
 	}
-	if lp.MinCurrent != minA {
-		t.Errorf("MinCurrent %v", lp.MinCurrent)
-	}
 	if lp.MaxCurrent != maxA {
 		t.Errorf("MaxCurrent %v", lp.MaxCurrent)
 	}