Repository: evcc-io/evcc
Pull request #6107 (2023-02-06 20:49:23)

Add loadpoint prioritisation
This PR adds a simple approach for prioritising load points, adding a `Priority` setting. Any load point with higher priority will get additional as much additional power budget as is currently consumed by lower-priority load points.

This will lead to excessive grid usage for a short period of time but will also provide a very simple-to-use mechanism.

In difference to https://github.com/evcc-io/evcc/issues/6106 this mechanism should only be used to prioritise loads under evcc control, while https://github.com/evcc-io/evcc/issues/6106 can be used for passive or automatically reactive loads.

TODO:

- [x] handle `minSoc` property of the low-prio load point such that this load point when below min soc will not provide additional power as it cannot reduce it's load anyway
- [x] same but be true for any charge mode except PV, that is Min+PV can only return power consumption above min


Patch:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 293a6ec9..4a52b831 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -81,7 +81,7 @@ jobs:
         uses: golangci/golangci-lint-action@v3
         with:
           version: latest
-          args: --timeout 3m
+          args: --timeout 5m
 
   build-ui:
     name: UI
diff --git a/cmd/root.go b/cmd/root.go
index c4ed0023..12d88806 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -21,6 +21,9 @@ import (
 	"github.com/evcc-io/evcc/util/pipe"
 	"github.com/evcc-io/evcc/util/sponsor"
 	"github.com/evcc-io/evcc/util/telemetry"
+	"github.com/fatih/structs"
+	"github.com/jeremywohl/flatten"
+	"golang.org/x/exp/maps"
 
 	_ "github.com/joho/godotenv/autoload"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -89,6 +92,12 @@ func initConfig() {
 	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
 	viper.AutomaticEnv() // read in environment variables that match
 
+	// register all known config keys
+	flat, _ := flatten.Flatten(structs.Map(conf), "", flatten.DotStyle)
+	for _, v := range maps.Keys(flat) {
+		viper.BindEnv(v)
+	}
+
 	// print version
 	util.LogLevel("info", nil)
 	log.INFO.Printf("evcc %s", server.FormattedVersion())
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 8e86fbe6..5eed52e4 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -103,6 +103,7 @@ type Loadpoint struct {
 	Mode       api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex
 
 	Title_            string   `mapstructure:"title"`    // UI title
+	Priority_         int      `mapstructure:"priority"` // Priority
 	ConfiguredPhases  int      `mapstructure:"phases"`   // Charger configured phase mode 0/1/3
 	ChargerRef        string   `mapstructure:"charger"`  // Charger reference
 	VehicleRef        string   `mapstructure:"vehicle"`  // Vehicle reference
diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go
index 29518939..20fea55c 100644
--- a/core/loadpoint/api.go
+++ b/core/loadpoint/api.go
@@ -6,6 +6,8 @@ import (
 	"github.com/evcc-io/evcc/api"
 )
 
+//go:generate mockgen -package loadpoint -destination mock.go -mock_names API=MockAPI github.com/evcc-io/evcc/core/loadpoint API
+
 // Controller gives access to loadpoint
 type Controller interface {
 	LoadpointControl(API)
@@ -15,6 +17,7 @@ type Controller interface {
 type API interface {
 	// Title returns the defined loadpoint title
 	Title() string
+	Priority() int
 
 	//
 	// status
@@ -76,6 +79,8 @@ type API interface {
 	HasChargeMeter() bool
 	// GetChargePower returns the current charging power
 	GetChargePower() float64
+	// GetChargePowerFlexibility returns the flexible amount of current charging power
+	GetChargePowerFlexibility() float64
 	// GetMinCurrent returns the min charging current
 	GetMinCurrent() float64
 	// SetMinCurrent sets the min charging current
diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go
new file mode 100644
index 00000000..991780e1
--- /dev/null
+++ b/core/loadpoint/mock.go
@@ -0,0 +1,520 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/evcc-io/evcc/core/loadpoint (interfaces: API)
+
+// Package loadpoint is a generated GoMock package.
+package loadpoint
+
+import (
+	reflect "reflect"
+	time "time"
+
+	api "github.com/evcc-io/evcc/api"
+	gomock "github.com/golang/mock/gomock"
+)
+
+// MockAPI is a mock of API interface.
+type MockAPI struct {
+	ctrl     *gomock.Controller
+	recorder *MockAPIMockRecorder
+}
+
+// MockAPIMockRecorder is the mock recorder for MockAPI.
+type MockAPIMockRecorder struct {
+	mock *MockAPI
+}
+
+// NewMockAPI creates a new mock instance.
+func NewMockAPI(ctrl *gomock.Controller) *MockAPI {
+	mock := &MockAPI{ctrl: ctrl}
+	mock.recorder = &MockAPIMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
+	return m.recorder
+}
+
+// GetChargePower mocks base method.
+func (m *MockAPI) GetChargePower() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetChargePower")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetChargePower indicates an expected call of GetChargePower.
+func (mr *MockAPIMockRecorder) GetChargePower() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePower", reflect.TypeOf((*MockAPI)(nil).GetChargePower))
+}
+
+// GetChargePowerFlexibility mocks base method.
+func (m *MockAPI) GetChargePowerFlexibility() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetChargePowerFlexibility")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetChargePowerFlexibility indicates an expected call of GetChargePowerFlexibility.
+func (mr *MockAPIMockRecorder) GetChargePowerFlexibility() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChargePowerFlexibility", reflect.TypeOf((*MockAPI)(nil).GetChargePowerFlexibility))
+}
+
+// GetDisableThreshold mocks base method.
+func (m *MockAPI) GetDisableThreshold() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetDisableThreshold")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetDisableThreshold indicates an expected call of GetDisableThreshold.
+func (mr *MockAPIMockRecorder) GetDisableThreshold() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDisableThreshold", reflect.TypeOf((*MockAPI)(nil).GetDisableThreshold))
+}
+
+// GetEnableThreshold mocks base method.
+func (m *MockAPI) GetEnableThreshold() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetEnableThreshold")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetEnableThreshold indicates an expected call of GetEnableThreshold.
+func (mr *MockAPIMockRecorder) GetEnableThreshold() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnableThreshold", reflect.TypeOf((*MockAPI)(nil).GetEnableThreshold))
+}
+
+// GetMaxCurrent mocks base method.
+func (m *MockAPI) GetMaxCurrent() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMaxCurrent")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetMaxCurrent indicates an expected call of GetMaxCurrent.
+func (mr *MockAPIMockRecorder) GetMaxCurrent() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxCurrent", reflect.TypeOf((*MockAPI)(nil).GetMaxCurrent))
+}
+
+// GetMaxPower mocks base method.
+func (m *MockAPI) GetMaxPower() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMaxPower")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetMaxPower indicates an expected call of GetMaxPower.
+func (mr *MockAPIMockRecorder) GetMaxPower() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxPower", reflect.TypeOf((*MockAPI)(nil).GetMaxPower))
+}
+
+// GetMinCurrent mocks base method.
+func (m *MockAPI) GetMinCurrent() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMinCurrent")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetMinCurrent indicates an expected call of GetMinCurrent.
+func (mr *MockAPIMockRecorder) GetMinCurrent() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinCurrent", reflect.TypeOf((*MockAPI)(nil).GetMinCurrent))
+}
+
+// GetMinPower mocks base method.
+func (m *MockAPI) GetMinPower() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMinPower")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetMinPower indicates an expected call of GetMinPower.
+func (mr *MockAPIMockRecorder) GetMinPower() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinPower", reflect.TypeOf((*MockAPI)(nil).GetMinPower))
+}
+
+// GetMinSoc mocks base method.
+func (m *MockAPI) GetMinSoc() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMinSoc")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// GetMinSoc indicates an expected call of GetMinSoc.
+func (mr *MockAPIMockRecorder) GetMinSoc() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinSoc", reflect.TypeOf((*MockAPI)(nil).GetMinSoc))
+}
+
+// GetMode mocks base method.
+func (m *MockAPI) GetMode() api.ChargeMode {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMode")
+	ret0, _ := ret[0].(api.ChargeMode)
+	return ret0
+}
+
+// GetMode indicates an expected call of GetMode.
+func (mr *MockAPIMockRecorder) GetMode() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMode", reflect.TypeOf((*MockAPI)(nil).GetMode))
+}
+
+// GetPhases mocks base method.
+func (m *MockAPI) GetPhases() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPhases")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// GetPhases indicates an expected call of GetPhases.
+func (mr *MockAPIMockRecorder) GetPhases() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPhases", reflect.TypeOf((*MockAPI)(nil).GetPhases))
+}
+
+// GetPlan mocks base method.
+func (m *MockAPI) GetPlan(arg0 time.Time, arg1 float64) (time.Duration, api.Rates, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPlan", arg0, arg1)
+	ret0, _ := ret[0].(time.Duration)
+	ret1, _ := ret[1].(api.Rates)
+	ret2, _ := ret[2].(error)
+	return ret0, ret1, ret2
+}
+
+// GetPlan indicates an expected call of GetPlan.
+func (mr *MockAPIMockRecorder) GetPlan(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlan", reflect.TypeOf((*MockAPI)(nil).GetPlan), arg0, arg1)
+}
+
+// GetPlannerUnit mocks base method.
+func (m *MockAPI) GetPlannerUnit() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPlannerUnit")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// GetPlannerUnit indicates an expected call of GetPlannerUnit.
+func (mr *MockAPIMockRecorder) GetPlannerUnit() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlannerUnit", reflect.TypeOf((*MockAPI)(nil).GetPlannerUnit))
+}
+
+// GetRemainingDuration mocks base method.
+func (m *MockAPI) GetRemainingDuration() time.Duration {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRemainingDuration")
+	ret0, _ := ret[0].(time.Duration)
+	return ret0
+}
+
+// GetRemainingDuration indicates an expected call of GetRemainingDuration.
+func (mr *MockAPIMockRecorder) GetRemainingDuration() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRemainingDuration", reflect.TypeOf((*MockAPI)(nil).GetRemainingDuration))
+}
+
+// GetRemainingEnergy mocks base method.
+func (m *MockAPI) GetRemainingEnergy() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRemainingEnergy")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetRemainingEnergy indicates an expected call of GetRemainingEnergy.
+func (mr *MockAPIMockRecorder) GetRemainingEnergy() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRemainingEnergy", reflect.TypeOf((*MockAPI)(nil).GetRemainingEnergy))
+}
+
+// GetStatus mocks base method.
+func (m *MockAPI) GetStatus() api.ChargeStatus {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetStatus")
+	ret0, _ := ret[0].(api.ChargeStatus)
+	return ret0
+}
+
+// GetStatus indicates an expected call of GetStatus.
+func (mr *MockAPIMockRecorder) GetStatus() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatus", reflect.TypeOf((*MockAPI)(nil).GetStatus))
+}
+
+// GetTargetEnergy mocks base method.
+func (m *MockAPI) GetTargetEnergy() float64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetTargetEnergy")
+	ret0, _ := ret[0].(float64)
+	return ret0
+}
+
+// GetTargetEnergy indicates an expected call of GetTargetEnergy.
+func (mr *MockAPIMockRecorder) GetTargetEnergy() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTargetEnergy", reflect.TypeOf((*MockAPI)(nil).GetTargetEnergy))
+}
+
+// GetTargetSoc mocks base method.
+func (m *MockAPI) GetTargetSoc() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetTargetSoc")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// GetTargetSoc indicates an expected call of GetTargetSoc.
+func (mr *MockAPIMockRecorder) GetTargetSoc() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTargetSoc", reflect.TypeOf((*MockAPI)(nil).GetTargetSoc))
+}
+
+// GetTargetTime mocks base method.
+func (m *MockAPI) GetTargetTime() time.Time {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetTargetTime")
+	ret0, _ := ret[0].(time.Time)
+	return ret0
+}
+
+// GetTargetTime indicates an expected call of GetTargetTime.
+func (mr *MockAPIMockRecorder) GetTargetTime() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTargetTime", reflect.TypeOf((*MockAPI)(nil).GetTargetTime))
+}
+
+// GetVehicle mocks base method.
+func (m *MockAPI) GetVehicle() api.Vehicle {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetVehicle")
+	ret0, _ := ret[0].(api.Vehicle)
+	return ret0
+}
+
+// GetVehicle indicates an expected call of GetVehicle.
+func (mr *MockAPIMockRecorder) GetVehicle() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVehicle", reflect.TypeOf((*MockAPI)(nil).GetVehicle))
+}
+
+// HasChargeMeter mocks base method.
+func (m *MockAPI) HasChargeMeter() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "HasChargeMeter")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// HasChargeMeter indicates an expected call of HasChargeMeter.
+func (mr *MockAPIMockRecorder) HasChargeMeter() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasChargeMeter", reflect.TypeOf((*MockAPI)(nil).HasChargeMeter))
+}
+
+// Priority mocks base method.
+func (m *MockAPI) Priority() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Priority")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// Priority indicates an expected call of Priority.
+func (mr *MockAPIMockRecorder) Priority() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Priority", reflect.TypeOf((*MockAPI)(nil).Priority))
+}
+
+// RemoteControl mocks base method.
+func (m *MockAPI) RemoteControl(arg0 string, arg1 RemoteDemand) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "RemoteControl", arg0, arg1)
+}
+
+// RemoteControl indicates an expected call of RemoteControl.
+func (mr *MockAPIMockRecorder) RemoteControl(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteControl", reflect.TypeOf((*MockAPI)(nil).RemoteControl), arg0, arg1)
+}
+
+// SetDisableThreshold mocks base method.
+func (m *MockAPI) SetDisableThreshold(arg0 float64) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetDisableThreshold", arg0)
+}
+
+// SetDisableThreshold indicates an expected call of SetDisableThreshold.
+func (mr *MockAPIMockRecorder) SetDisableThreshold(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDisableThreshold", reflect.TypeOf((*MockAPI)(nil).SetDisableThreshold), arg0)
+}
+
+// SetEnableThreshold mocks base method.
+func (m *MockAPI) SetEnableThreshold(arg0 float64) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetEnableThreshold", arg0)
+}
+
+// SetEnableThreshold indicates an expected call of SetEnableThreshold.
+func (mr *MockAPIMockRecorder) SetEnableThreshold(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEnableThreshold", reflect.TypeOf((*MockAPI)(nil).SetEnableThreshold), arg0)
+}
+
+// SetMaxCurrent mocks base method.
+func (m *MockAPI) SetMaxCurrent(arg0 float64) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetMaxCurrent", arg0)
+}
+
+// SetMaxCurrent indicates an expected call of SetMaxCurrent.
+func (mr *MockAPIMockRecorder) SetMaxCurrent(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxCurrent", reflect.TypeOf((*MockAPI)(nil).SetMaxCurrent), arg0)
+}
+
+// SetMinCurrent mocks base method.
+func (m *MockAPI) SetMinCurrent(arg0 float64) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetMinCurrent", arg0)
+}
+
+// SetMinCurrent indicates an expected call of SetMinCurrent.
+func (mr *MockAPIMockRecorder) SetMinCurrent(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMinCurrent", reflect.TypeOf((*MockAPI)(nil).SetMinCurrent), arg0)
+}
+
+// SetMinSoc mocks base method.
+func (m *MockAPI) SetMinSoc(arg0 int) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetMinSoc", arg0)
+}
+
+// SetMinSoc indicates an expected call of SetMinSoc.
+func (mr *MockAPIMockRecorder) SetMinSoc(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMinSoc", reflect.TypeOf((*MockAPI)(nil).SetMinSoc), arg0)
+}
+
+// SetMode mocks base method.
+func (m *MockAPI) SetMode(arg0 api.ChargeMode) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetMode", arg0)
+}
+
+// SetMode indicates an expected call of SetMode.
+func (mr *MockAPIMockRecorder) SetMode(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMode", reflect.TypeOf((*MockAPI)(nil).SetMode), arg0)
+}
+
+// SetPhases mocks base method.
+func (m *MockAPI) SetPhases(arg0 int) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SetPhases", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// SetPhases indicates an expected call of SetPhases.
+func (mr *MockAPIMockRecorder) SetPhases(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhases", reflect.TypeOf((*MockAPI)(nil).SetPhases), arg0)
+}
+
+// SetTargetEnergy mocks base method.
+func (m *MockAPI) SetTargetEnergy(arg0 float64) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetTargetEnergy", arg0)
+}
+
+// SetTargetEnergy indicates an expected call of SetTargetEnergy.
+func (mr *MockAPIMockRecorder) SetTargetEnergy(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTargetEnergy", reflect.TypeOf((*MockAPI)(nil).SetTargetEnergy), arg0)
+}
+
+// SetTargetSoc mocks base method.
+func (m *MockAPI) SetTargetSoc(arg0 int) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetTargetSoc", arg0)
+}
+
+// SetTargetSoc indicates an expected call of SetTargetSoc.
+func (mr *MockAPIMockRecorder) SetTargetSoc(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTargetSoc", reflect.TypeOf((*MockAPI)(nil).SetTargetSoc), arg0)
+}
+
+// SetTargetTime mocks base method.
+func (m *MockAPI) SetTargetTime(arg0 time.Time) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SetTargetTime", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// SetTargetTime indicates an expected call of SetTargetTime.
+func (mr *MockAPIMockRecorder) SetTargetTime(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTargetTime", reflect.TypeOf((*MockAPI)(nil).SetTargetTime), arg0)
+}
+
+// SetVehicle mocks base method.
+func (m *MockAPI) SetVehicle(arg0 api.Vehicle) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetVehicle", arg0)
+}
+
+// SetVehicle indicates an expected call of SetVehicle.
+func (mr *MockAPIMockRecorder) SetVehicle(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVehicle", reflect.TypeOf((*MockAPI)(nil).SetVehicle), arg0)
+}
+
+// StartVehicleDetection mocks base method.
+func (m *MockAPI) StartVehicleDetection() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "StartVehicleDetection")
+}
+
+// StartVehicleDetection indicates an expected call of StartVehicleDetection.
+func (mr *MockAPIMockRecorder) StartVehicleDetection() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartVehicleDetection", reflect.TypeOf((*MockAPI)(nil).StartVehicleDetection))
+}
+
+// Title mocks base method.
+func (m *MockAPI) Title() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Title")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Title indicates an expected call of Title.
+func (mr *MockAPIMockRecorder) Title() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Title", reflect.TypeOf((*MockAPI)(nil).Title))
+}
diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go
index 04a15c53..2a32e5c1 100644
--- a/core/loadpoint_api.go
+++ b/core/loadpoint_api.go
@@ -3,6 +3,7 @@ package core
 import (
 	"errors"
 	"fmt"
+	"math"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -17,6 +18,11 @@ func (lp *Loadpoint) Title() string {
 	return lp.Title_
 }
 
+// Priority returns the loadpoint priority
+func (lp *Loadpoint) Priority() int {
+	return lp.Priority_
+}
+
 // GetStatus returns the charging status
 func (lp *Loadpoint) GetStatus() api.ChargeStatus {
 	lp.Lock()
@@ -285,6 +291,22 @@ func (lp *Loadpoint) GetChargePower() float64 {
 	return lp.chargePower
 }
 
+// GetChargePowerFlexibility returns the flexible amount of current charging power
+func (lp *Loadpoint) GetChargePowerFlexibility() float64 {
+	// no locking
+	mode := lp.GetMode()
+	if mode == api.ModeNow || !lp.charging() || lp.minSocNotReached() {
+		return 0
+	}
+
+	if mode == api.ModePV {
+		return lp.GetChargePower()
+	}
+
+	// MinPV mode
+	return math.Max(0, lp.GetChargePower()-lp.GetMinPower())
+}
+
 // GetMinCurrent returns the min loadpoint current
 func (lp *Loadpoint) GetMinCurrent() float64 {
 	lp.Lock()
diff --git a/core/prioritizer/prioritizer.go b/core/prioritizer/prioritizer.go
new file mode 100644
index 00000000..334dac20
--- /dev/null
+++ b/core/prioritizer/prioritizer.go
@@ -0,0 +1,34 @@
+package prioritizer
+
+import (
+	"github.com/evcc-io/evcc/core/loadpoint"
+)
+
+type Prioritizer struct {
+	demand map[loadpoint.API]float64
+}
+
+func New() *Prioritizer {
+	return &Prioritizer{
+		demand: make(map[loadpoint.API]float64),
+	}
+}
+
+func (p *Prioritizer) UpdateChargePowerFlexibility(lp loadpoint.API) {
+	if power := lp.GetChargePowerFlexibility(); power >= 0 {
+		p.demand[lp] = power
+	}
+}
+
+func (p *Prioritizer) GetChargePowerFlexibility(lp loadpoint.API) float64 {
+	prio := lp.Priority()
+
+	var reduceBy float64
+	for lp, power := range p.demand {
+		if lp.Priority() < prio {
+			reduceBy += power
+		}
+	}
+
+	return reduceBy
+}
diff --git a/core/site.go b/core/site.go
index 95ba444a..dac066e3 100644
--- a/core/site.go
+++ b/core/site.go
@@ -14,6 +14,7 @@ import (
 	"github.com/evcc-io/evcc/core/db"
 	"github.com/evcc-io/evcc/core/loadpoint"
 	"github.com/evcc-io/evcc/core/planner"
+	"github.com/evcc-io/evcc/core/prioritizer"
 	"github.com/evcc-io/evcc/push"
 	serverdb "github.com/evcc-io/evcc/server/db"
 	"github.com/evcc-io/evcc/tariff"
@@ -25,6 +26,7 @@ const standbyPower = 10 // consider less than 10W as charger in standby
 
 // Updater abstracts the Loadpoint implementation for testing
 type Updater interface {
+	loadpoint.API
 	Update(availablePower float64, batteryBuffered bool)
 }
 
@@ -68,6 +70,7 @@ type Site struct {
 	tariffs     tariff.Tariffs           // Tariff
 	loadpoints  []*Loadpoint             // Loadpoints
 	coordinator *coordinator.Coordinator // Vehicles
+	prioritizer *prioritizer.Prioritizer // Power budgets
 	savings     *Savings                 // Savings
 
 	// cached state
@@ -108,6 +111,7 @@ func NewSiteFromConfig(
 	site.loadpoints = loadpoints
 	site.tariffs = tariffs
 	site.coordinator = coordinator.New(log, vehicles)
+	site.prioritizer = prioritizer.New()
 	site.savings = NewSavings(tariffs)
 
 	// upload telemetry on shutdown
@@ -485,7 +489,7 @@ func (site *Site) updateMeters() error {
 
 // sitePower returns the net power exported by the site minus a residual margin.
 // negative values mean grid: export, battery: charging
-func (site *Site) sitePower(totalChargePower float64) (float64, error) {
+func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, error) {
 	if err := site.updateMeters(); err != nil {
 		return 0, err
 	}
@@ -537,6 +541,12 @@ func (site *Site) sitePower(totalChargePower float64) (float64, error) {
 	site.publish("auxPower", auxPower)
 	sitePower -= auxPower
 
+	// handle priority
+	if flexiblePower > 0 {
+		site.log.DEBUG.Printf("giving loadpoint priority for additional: %.0fW", flexiblePower)
+		sitePower -= flexiblePower
+	}
+
 	site.log.DEBUG.Printf("site power: %.0fW", sitePower)
 
 	return sitePower, nil
@@ -609,9 +619,17 @@ func (site *Site) update(lp Updater) {
 	for _, lp := range site.loadpoints {
 		lp.UpdateChargePower()
 		totalChargePower += lp.GetChargePower()
+
+		site.prioritizer.UpdateChargePowerFlexibility(lp)
+	}
+
+	// prioritize if possible
+	var flexiblePower float64
+	if lp.GetMode() == api.ModePV {
+		flexiblePower = site.prioritizer.GetChargePowerFlexibility(lp)
 	}
 
-	if sitePower, err := site.sitePower(totalChargePower); err == nil {
+	if sitePower, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
 		lp.Update(sitePower, site.batteryBuffered)
 
 		// ignore negative pvPower values as that means it is not an energy source but consumption
diff --git a/docker/bin/entrypoint.sh b/docker/bin/entrypoint.sh
index 8f537ec0..a346c7be 100755
--- a/docker/bin/entrypoint.sh
+++ b/docker/bin/entrypoint.sh
@@ -16,7 +16,7 @@ if [ -f ${HASSIO_OPTIONSFILE} ]; then
     else
         if [ "${SQLITE_FILE}" ]; then
             echo "starting evcc: 'EVCC_DATABASE_DSN=${SQLITE_FILE} evcc --config ${CONFIG}'"
-            exec EVCC_DATABASE_DSN="${SQLITE_FILE}" evcc --config "${CONFIG}"
+            exec env EVCC_DATABASE_DSN="${SQLITE_FILE}" evcc --config "${CONFIG}"
         else
             echo "starting evcc: 'evcc --config ${CONFIG}'"
             exec evcc --config "${CONFIG}"
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 11c1a937..ab8cdece 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -119,6 +119,12 @@ loadpoints:
     mode: "off" # set default charge mode, use "off" to disable by default if charger is publicly available
     # vehicle: car1 # set default vehicle (disables vehicle detection)
     resetOnDisconnect: true # set defaults when vehicle disconnects
+    phases: 3 # electrical connection (normal charger: default 3 for 3 phase, 1p3p charger: 0 for "auto" or 1/3 for fixed phases)
+    minCurrent: 6 # minimum charge current (default 6A)
+    maxCurrent: 16 # maximum charge current (default 16A)
+
+    # remaining settings are experts-only and best left at default values
+    priority: 0 # relative priority for concurrent charging in PV mode with multiple loadpoints (higher values have higher priority)
     soc:
       # polling defines usage of the vehicle APIs
       # Modifying the default settings it NOT recommended. It MAY deplete your vehicle's battery
@@ -132,7 +138,6 @@ loadpoints:
         # poll interval defines how often the vehicle API may be polled if NOT charging
         interval: 60m
       estimate: true # set false to disable interpolating between api updates (not recommended)
-    phases: 3 # electrical connection (normal charger: default 3 for 3 phase, 1p3p charger: 0 for "auto" or 1/3 for fixed phases)
     enable: # pv mode enable behavior
       delay: 1m # threshold must be exceeded for this long
       threshold: 0 # grid power threshold (in Watts, negative=export). If zero, export must exceed minimum charge power to enable
@@ -140,8 +145,6 @@ loadpoints:
       delay: 3m # threshold must be exceeded for this long
       threshold: 0 # maximum import power (W)
     guardDuration: 5m # switch charger contactor not more often than this (default 5m)
-    minCurrent: 6 # minimum charge current (default 6A)
-    maxCurrent: 16 # maximum charge current (default 16A)
 
 # tariffs are the fixed or variable tariffs
 # cheap (tibber/awattar) can be used to define a tariff rate considered cheap enough for charging
diff --git a/provider/cache.go b/provider/cache.go
index bcfaffce..99b790b1 100644
--- a/provider/cache.go
+++ b/provider/cache.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"errors"
+	"math"
 	"sync"
 	"time"
 
@@ -16,7 +17,10 @@ var (
 	log = util.NewLogger("cache")
 )
 
-const reset = "reset"
+const (
+	reset           = "reset"
+	backoffDuration = 5 * time.Second
+)
 
 func ResetCached() {
 	bus.Publish(reset)
@@ -24,13 +28,15 @@ func ResetCached() {
 
 // cached wraps a getter with a cache
 type cached[T any] struct {
-	mux     sync.Mutex
-	clock   clock.Clock
-	updated time.Time
-	cache   time.Duration
-	g       func() (T, error)
-	val     T
-	err     error
+	mux            sync.Mutex
+	clock          clock.Clock
+	updated        time.Time
+	retried        time.Time
+	cache          time.Duration
+	backoffCounter int
+	g              func() (T, error)
+	val            T
+	err            error
 }
 
 // Cached wraps a getter with a cache
@@ -50,8 +56,9 @@ var _ Cacheable[int64] = (*cached[int64])(nil)
 // ResettableCached wraps a getter with a cache. It returns a `Cacheable`.
 // Instead of the cached getter, the `Get()` and `Reset()` methods are exposed.
 func ResettableCached[T any](g func() (T, error), cache time.Duration) *cached[T] {
+	clock := clock.New()
 	c := &cached[T]{
-		clock: clock.New(),
+		clock: clock,
 		cache: cache,
 		g:     g,
 	}
@@ -66,6 +73,11 @@ func (c *cached[T]) Get() (T, error) {
 	if c.mustUpdate() {
 		c.val, c.err = c.g()
 		c.updated = c.clock.Now()
+		c.retried = c.clock.Now()
+
+		if c.err == nil {
+			c.backoffCounter = 0
+		}
 	}
 
 	return c.val, c.err
@@ -74,9 +86,22 @@ func (c *cached[T]) Get() (T, error) {
 func (c *cached[T]) Reset() {
 	c.mux.Lock()
 	c.updated = time.Time{}
+	c.retried = time.Time{}
 	c.mux.Unlock()
 }
 
 func (c *cached[T]) mustUpdate() bool {
-	return c.clock.Since(c.updated) > c.cache || errors.Is(c.err, api.ErrMustRetry)
+	return c.clock.Since(c.updated) > c.cache ||
+		errors.Is(c.err, api.ErrMustRetry) ||
+		c.err != nil && c.shouldRetryWithBackoff()
+}
+
+// shouldRetryWithBackoff returns true when exponential back-off duration has elapsed since last retry
+func (c *cached[T]) shouldRetryWithBackoff() bool {
+	if c.clock.Since(c.retried) > backoffDuration*time.Duration(math.Pow(2, float64(c.backoffCounter))) {
+		c.backoffCounter++
+		return true
+	}
+
+	return false
 }
diff --git a/schema.json b/schema.json
index ef6773dc..e8134cd3 100644
--- a/schema.json
+++ b/schema.json
@@ -228,6 +228,18 @@
           "meter": {
             "type": "string"
           },
+          "minCurrent": {
+            "type": "integer"
+          },
+          "maxCurrent": {
+            "type": "integer"
+          },
+          "phases": {
+            "type": "integer"
+          },
+          "priority": {
+            "type": "integer"
+          },
           "vehicle": {
             "type": "string"
           },
@@ -250,15 +262,6 @@
               }
             }
           },
-          "phases": {
-            "type": "integer"
-          },
-          "minCurrent": {
-            "type": "integer"
-          },
-          "maxCurrent": {
-            "type": "integer"
-          },
           "guardDuration": {
             "$ref": "#/definitions/duration"
           },


Test patch:
diff --git a/core/prioritizer/prioritizer_test.go b/core/prioritizer/prioritizer_test.go
new file mode 100644
index 00000000..f3d322c6
--- /dev/null
+++ b/core/prioritizer/prioritizer_test.go
@@ -0,0 +1,36 @@
+package prioritizer
+
+import (
+	"testing"
+
+	"github.com/evcc-io/evcc/core/loadpoint"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPrioritzer(t *testing.T) {
+	ctrl := gomock.NewController(t)
+
+	p := New()
+
+	lo := loadpoint.NewMockAPI(ctrl)
+	lo.EXPECT().Priority().Return(0).AnyTimes()
+
+	hi := loadpoint.NewMockAPI(ctrl)
+	hi.EXPECT().Priority().Return(1).AnyTimes()
+
+	// no additional power available
+	lo.EXPECT().GetChargePowerFlexibility().Return(300.0)
+	p.UpdateChargePowerFlexibility(lo)
+	assert.Equal(t, 0.0, p.GetChargePowerFlexibility(lo))
+
+	// additional power available
+	hi.EXPECT().GetChargePowerFlexibility().Return(1e3)
+	p.UpdateChargePowerFlexibility(hi)
+	assert.Equal(t, 300.0, p.GetChargePowerFlexibility(hi))
+
+	// additional power removed
+	lo.EXPECT().GetChargePowerFlexibility().Return(0.0)
+	p.UpdateChargePowerFlexibility(lo)
+	assert.Equal(t, 0.0, p.GetChargePowerFlexibility(hi))
+}
diff --git a/provider/cache_test.go b/provider/cache_test.go
index 49e26855..059ae722 100644
--- a/provider/cache_test.go
+++ b/provider/cache_test.go
@@ -6,6 +6,8 @@ import (
 	"time"
 
 	"github.com/benbjohnson/clock"
+	"github.com/evcc-io/evcc/api"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestCachedGetter(t *testing.T) {
@@ -77,3 +79,51 @@ func TestCacheReset(t *testing.T) {
 	clock.Add(10*time.Minute + 1)
 	test(3)
 }
+
+func TestRetryWithBackoff(t *testing.T) {
+	tests := []struct {
+		deltaTime      time.Duration
+		returnError    bool
+		functionCalled bool
+	}{
+		{0 * time.Second, true, true},
+		{4 * time.Second, true, false},
+		{6 * time.Second, true, true},
+		{9 * time.Second, true, false},
+		{11 * time.Second, true, true},
+		{14 * time.Second, true, false},
+		{16 * time.Second, false, true},
+		{16 * time.Minute, true, true},
+		{4 * time.Second, true, false},
+		{6 * time.Second, true, true},
+	}
+
+	var returnError, functionCalled bool
+
+	g := func() (int64, error) {
+		functionCalled = true
+		if returnError {
+			return 1, api.ErrTimeout
+		}
+		return 1, nil
+	}
+
+	c := ResettableCached(g, 15*time.Minute)
+	clock := clock.NewMock()
+	c.clock = clock
+
+	for _, tt := range tests {
+		functionCalled = false
+		returnError = tt.returnError
+
+		clock.Add(tt.deltaTime)
+
+		_, err := c.Get()
+
+		if returnError {
+			assert.Error(t, err)
+		}
+
+		assert.Equal(t, tt.functionCalled, functionCalled)
+	}
+}