Repository: evcc-io/evcc
Pull request #6267 (2023-02-16 09:00:28)

allow one last soc update if stopped charging
Siehe: https://github.com/evcc-io/evcc/pull/6238#discussion_r1106395670

Das Ganze erlaubt jetzt ein weiteren Soc Update, wenn vorher geladen wurde und das Auto noch verbunden ist und der TargetSoc noch nicht erreicht ist laut letztem Update.
Dieses Update muss aber auch wieder `soc.poll.interval` warten.

Ich habe die Funktion jetzt auch zu mehreren Returns gerefactored um das ganze transparenter zu machen.
 Das hat auch den Vorteil das beim Laden nicht jedes mal das gleiche `60m` (`soc.poll.interval`) - `10s` (control `interval`) ausgegeben wird:
```
next soc poll remaining time: 59m50s
```
(wie [hier](https://github.com/evcc-io/evcc/discussions/6173#discussioncomment-4953818))

nename0: Bin mir nicht sicher vielleicht sollte man doch nicht auf `soc.poll.interval` warten.
Das ursprüngliche Issue #4220 hatte ja kein Soc Estimator, jetzt mit Soc Estimator macht dass die Sache noch komplizierter.
Beispiel:
targetSoc=95 wenn der Soc Estimator korrekt funktioniert und beim letzten Soc Update vehicleSoc=94 war, dann wir geladen bis Soc Estimator sagt vehicleSoc=95. 
Soll dann jetzt der Soc nochmal geupdated werden? Aktuell würde er nicht (wg. vehicleSoc >= targetSoc)
Jetzt stellt der Nutzer auf targetSoc=100 aber kein Solar Überschuss deshalb wird nicht geladen.
Soll dann nach `soc.poll.interval` oder `vehicle.cache` nochmal geupdated werden? Aktuell würde nach `soc.poll.interval` geupdated werden, dannach aber nicht mehr solange kein Ladevorgang.

Weiterhin denke ich aber auch, dass das ursprüngliche Issue #4220 bei eingeschaltetem Soc Estimator nie passiert wäre. Man könnte deshalb auch einfach den PR #4656 reverten.
Ist halt die Frage ob man für die paar Leute die den Soc Estimator ausgeschaltet haben das jetzt fixen muss.

Ich denke aber so wie es aktuell ist kann es nicht bleiben, dass trotz `mode: charging` auch ohne Ladevorgang geupdated wird.
Hofyyy: Also meinetwegen kann der PE wieder raus. Da ich dein Problem sehe. 

 Ich behaupte den Estimator gab es damals auch schon. Es ist eigentlich auch kein Problem mit dem estimator, sondern mit dem Vehicle cache. Der estimator liegt ja nie ganz richtig. so dass man am ende einmal das auto fragen müsste. Wenn nur das auto das laden beendet, ist es halt nicht mehr charging und man steht auf einem wert der ggf. 14min alt sein könnte. 
Hofyyy: @nename0 Komm doch gerne mal in den Slack. Ich hatte mir das thema sehr im detail angeschaut und auch schon einen richtigen PR erstellt. Den können wir ggf. ja mal für dich zur info durchgehen. Und den cache müssen wir flushbar machen :-)
nename0: @Hofyyy Bin drin und wo finde ich das jetzt?

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/Makefile b/Makefile
index c018d14a..e5c2d1e1 100644
--- a/Makefile
+++ b/Makefile
@@ -111,10 +111,9 @@ gokrazy::
 	go install github.com/gokrazy/tools/cmd/gokr-packer@main
 	mkdir -p flags/github.com/gokrazy/breakglass
 	echo "-forward=private-network" > flags/github.com/gokrazy/breakglass/flags.txt
-	mkdir -p flags/github.com/evcc-io/evcc
-	echo "--sqlite=/perm/evcc.db" > flags/github.com/evcc-io/evcc/flags.txt
 	mkdir -p env/github.com/evcc-io/evcc
 	echo "EVCC_NETWORK_PORT=80" > env/github.com/evcc-io/evcc/env.txt
+	echo "EVCC_DATABASE_DSN=/perm/evcc.db" >> env/github.com/evcc-io/evcc/env.txt
 	mkdir -p buildflags/github.com/evcc-io/evcc
 	echo "$(BUILD_TAGS),gokrazy" > buildflags/github.com/evcc-io/evcc/buildflags.txt
 	echo "-ldflags=$(LD_FLAGS)" >> buildflags/github.com/evcc-io/evcc/buildflags.txt
diff --git a/assets/css/app.css b/assets/css/app.css
index aca8d101..bfca86e7 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -191,6 +191,13 @@ a:hover {
 	--bs-btn-line-height: 2;
 }
 
+.dark .btn-outline-secondary {
+	--bs-btn-color: var(--bs-gray-bright);
+	--bs-btn-border-color: var(--bs-gray-bright);
+	--bs-btn-hover-bg: var(--bs-gray-bright);
+	--bs-btn-hover-color: var(--bs-gray-dark);
+}
+
 .text-evcc {
 	color: var(--evcc-dark-green);
 }
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index 456adebb..9382bd35 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -112,7 +112,11 @@
 							:vehicleIcons="vehicleIcons"
 							:power="loadpointsPower"
 							:powerInKw="powerInKw"
-							:details="detailsValue(tariffEffectivePrice, tariffEffectiveCo2)"
+							:details="
+								activeLoadpointsCount
+									? detailsValue(tariffEffectivePrice, tariffEffectiveCo2)
+									: undefined
+							"
 							:detailsFmt="detailsFmt"
 							:detailsTooltip="
 								detailsTooltip(tariffEffectivePrice, tariffEffectiveCo2)
diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue
index fb4cd91d..57745dd6 100644
--- a/assets/js/components/Energyflow/Visualization.vue
+++ b/assets/js/components/Energyflow/Visualization.vue
@@ -20,7 +20,7 @@
 				:style="{ width: widthTotal(selfConsumptionAdjusted) }"
 			>
 				<AnimatedNumber
-					v-if="selfConsumption"
+					v-if="selfConsumption && visualizationReady"
 					class="power"
 					:to="selfConsumption"
 					:format="fmtBarValue"
@@ -31,7 +31,7 @@
 				:style="{ width: widthTotal(gridImportAdjusted) }"
 			>
 				<AnimatedNumber
-					v-if="gridImport"
+					v-if="gridImport && visualizationReady"
 					class="power"
 					:to="gridImport"
 					:format="fmtBarValue"
@@ -42,7 +42,7 @@
 				:style="{ width: widthTotal(pvExportAdjusted) }"
 			>
 				<AnimatedNumber
-					v-if="pvExport"
+					v-if="pvExport && visualizationReady"
 					class="power"
 					:to="pvExport"
 					:format="fmtBarValue"
@@ -102,7 +102,7 @@ export default {
 		vehicleIcons: { type: Array },
 	},
 	data: function () {
-		return { width: 0, visualizationReady: false };
+		return { width: 0 };
 	},
 	computed: {
 		gridExport: function () {
@@ -123,15 +123,11 @@ export default {
 		totalAdjusted: function () {
 			return this.gridImportAdjusted + this.selfConsumptionAdjusted + this.pvExportAdjusted;
 		},
-	},
-	watch: {
-		totalAdjusted: function () {
-			if (!this.visualizationReady && this.totalAdjusted > 0)
-				setTimeout(() => {
-					this.visualizationReady = true;
-				}, 500);
+		visualizationReady: function () {
+			return this.totalAdjusted > 0 && this.width > 0;
 		},
 	},
+
 	mounted: function () {
 		this.$nextTick(function () {
 			window.addEventListener("resize", this.updateElementWidth);
@@ -147,8 +143,8 @@ export default {
 			return (100 / this.totalAdjusted) * power + "%";
 		},
 		fmtBarValue: function (watt) {
-			const valueInKw = this.powerLabelEnoughSpace(watt);
-			return this.fmtKw(watt, this.powerInKw, valueInKw);
+			const withUnit = this.powerLabelEnoughSpace(watt);
+			return this.fmtKw(watt, this.powerInKw, withUnit);
 		},
 		powerLabelAvailableSpace(power) {
 			if (this.totalAdjusted === 0) return 0;
@@ -158,9 +154,6 @@ export default {
 		powerLabelEnoughSpace(power) {
 			return this.powerLabelAvailableSpace(power) > 60;
 		},
-		powerLabelSomeSpace(power) {
-			return this.powerLabelAvailableSpace(power) > 35;
-		},
 		hideLabelIcon(power, minWidth = 32) {
 			if (this.totalAdjusted === 0) return true;
 			const percent = (100 / this.totalAdjusted) * power;
@@ -239,12 +232,12 @@ html.dark .grid-import {
 	white-space: nowrap;
 	overflow: hidden;
 }
-.visualization--ready ::v-deep(.label-bar) {
+.visualization--ready :deep(.label-bar) {
 	transition-property: width, opacity;
 	transition-duration: var(--evcc-transition-medium), var(--evcc-transition-fast);
 	transition-timing-function: linear, ease;
 }
-.visualization--ready ::v-deep(.label-bar-icon) {
+.visualization--ready :deep(.label-bar-icon) {
 	transition-duration: var(--evcc-transition-very-fast), 500ms;
 }
 </style>
diff --git a/assets/js/components/TelemetrySettings.vue b/assets/js/components/TelemetrySettings.vue
index 02560761..e84ad09e 100644
--- a/assets/js/components/TelemetrySettings.vue
+++ b/assets/js/components/TelemetrySettings.vue
@@ -90,7 +90,7 @@ export default {
 .form-check-label {
 	max-width: 100%;
 }
-.errorMessage::v-deep pre {
+.errorMessage :deep(pre) {
 	text-overflow: ellipsis;
 	font-size: 1em;
 }
diff --git a/assets/js/components/VehicleTitle.vue b/assets/js/components/VehicleTitle.vue
index fe754d54..aacf408a 100644
--- a/assets/js/components/VehicleTitle.vue
+++ b/assets/js/components/VehicleTitle.vue
@@ -132,7 +132,7 @@ export default {
 .spin {
 	animation: rotation 1s infinite cubic-bezier(0.37, 0, 0.63, 1);
 }
-.spin ::v-deep(svg) {
+.spin :deep(svg) {
 	/* workaround to fix the not perfectly centered shopicon. Remove once its fixed in @h2d2/shopicons */
 	transform: translateY(-0.7px);
 }
diff --git a/cmd/flags.go b/cmd/flags.go
index 6a795d9b..3f9066b7 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -8,9 +8,6 @@ import (
 )
 
 const (
-	flagSqlite            = "sqlite"
-	flagSqliteDescription = "SQlite database file"
-
 	flagHeaders            = "log-headers"
 	flagHeadersDescription = "Log headers"
 
diff --git a/cmd/root.go b/cmd/root.go
index 3d95be8d..8d5f85e7 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"
@@ -57,9 +60,6 @@ func init() {
 	rootCmd.PersistentFlags().Bool(flagHeaders, false, flagHeadersDescription)
 
 	// config file options
-	rootCmd.PersistentFlags().String(flagSqlite, conf.Database.Dsn, flagSqliteDescription)
-	bindP(rootCmd, "database.dsn", flagSqlite)
-
 	rootCmd.PersistentFlags().StringP("log", "l", "info", "Log level (fatal, error, warn, info, debug, trace)")
 	bindP(rootCmd, "log")
 
@@ -89,8 +89,15 @@ func initConfig() {
 	}
 
 	viper.SetEnvPrefix("evcc")
+	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())
@@ -207,7 +214,7 @@ func runRoot(cmd *cobra.Command, args []string) {
 	// setup messaging
 	var pushChan chan push.Event
 	if err == nil {
-		pushChan, err = configureMessengers(conf.Messaging, cache)
+		pushChan, err = configureMessengers(conf.Messaging, valueChan, cache)
 	}
 
 	// run shutdown functions on stop
diff --git a/cmd/setup.go b/cmd/setup.go
index 36782e01..3764e091 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -72,8 +72,8 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) {
 	}
 
 	// setup sponsorship (allow env override)
-	if token := viper.GetString("sponsortoken"); err == nil && token != "" {
-		err = sponsor.ConfigureSponsorship(token)
+	if err == nil && conf.SponsorToken != "" {
+		err = sponsor.ConfigureSponsorship(conf.SponsorToken)
 	}
 
 	// setup translations
@@ -83,10 +83,6 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) {
 
 	// setup persistence
 	if err == nil && conf.Database.Dsn != "" {
-		if flag := cmd.Flags().Lookup(flagSqlite); flag != nil && flag.Changed {
-			conf.Database.Type = "sqlite"
-			conf.Database.Dsn = flag.Value.String()
-		}
 		err = configureDatabase(conf.Database)
 	}
 
@@ -207,7 +203,7 @@ func configureEEBus(conf map[string]interface{}) error {
 }
 
 // setup messaging
-func configureMessengers(conf messagingConfig, cache *util.Cache) (chan push.Event, error) {
+func configureMessengers(conf messagingConfig, valueChan chan util.Param, cache *util.Cache) (chan push.Event, error) {
 	messageChan := make(chan push.Event, 1)
 
 	messageHub, err := push.NewHub(conf.Events, cache)
@@ -223,7 +219,7 @@ func configureMessengers(conf messagingConfig, cache *util.Cache) (chan push.Eve
 		messageHub.Add(impl)
 	}
 
-	go messageHub.Run(messageChan)
+	go messageHub.Run(messageChan, valueChan)
 
 	return messageChan, nil
 }
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 8e86fbe6..93fe25ab 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
@@ -118,15 +119,16 @@ type Loadpoint struct {
 	MaxCurrent    float64       // Max allowed current. Physically ensured by the charger
 	GuardDuration time.Duration // charger enable/disable minimum holding time
 
-	enabled             bool      // Charger enabled state
-	phases              int       // Charger enabled phases, guarded by mutex
-	measuredPhases      int       // Charger physically measured phases
-	chargeCurrent       float64   // Charger current limit
-	guardUpdated        time.Time // Charger enabled/disabled timestamp
-	socUpdated          time.Time // Soc updated timestamp (poll: connected)
-	vehicleDetect       time.Time // Vehicle connected timestamp
-	vehicleDetectTicker *clock.Ticker
-	vehicleIdentifier   string
+	enabled                  bool      // Charger enabled state
+	phases                   int       // Charger enabled phases, guarded by mutex
+	measuredPhases           int       // Charger physically measured phases
+	chargeCurrent            float64   // Charger current limit
+	guardUpdated             time.Time // Charger enabled/disabled timestamp
+	socUpdated               time.Time // Soc updated timestamp (poll: connected)
+	didChargeOnLastSocUpdate bool      // There was a charge process when Soc was updated last
+	vehicleDetect            time.Time // Vehicle connected timestamp
+	vehicleDetectTicker      *clock.Ticker
+	vehicleIdentifier        string
 
 	charger     api.Charger
 	chargeTimer api.ChargeTimer
@@ -1303,17 +1305,34 @@ func (lp *Loadpoint) publishChargeProgress() {
 
 // socPollAllowed validates charging state against polling mode
 func (lp *Loadpoint) socPollAllowed() bool {
+	// always update soc when charging
+	if lp.charging() {
+		lp.didChargeOnLastSocUpdate = true
+		return true
+	}
+
+	// update if connected and soc unknown
+	if lp.connected() && lp.socUpdated.IsZero() {
+		return true
+	}
+
 	remaining := lp.Soc.Poll.Interval - lp.clock.Since(lp.socUpdated)
 
 	honourUpdateInterval := lp.Soc.Poll.Mode == pollAlways ||
-		lp.Soc.Poll.Mode == pollConnected && lp.connected() ||
-		lp.Soc.Poll.Mode == pollCharging && lp.connected() && (lp.vehicleSoc < float64(lp.Soc.target))
+		lp.connected() && (lp.Soc.Poll.Mode == pollConnected ||
+			// for mode charging allow one last soc update if did charge previously to not rely on soc estimator too much
+			lp.Soc.Poll.Mode == pollCharging && lp.didChargeOnLastSocUpdate)
 
-	if honourUpdateInterval && remaining > 0 {
-		lp.log.DEBUG.Printf("next soc poll remaining time: %v", remaining.Truncate(time.Second))
+	if honourUpdateInterval {
+		if remaining > 0 {
+			lp.log.DEBUG.Printf("next soc poll remaining time: %v", remaining.Truncate(time.Second))
+		} else {
+			lp.didChargeOnLastSocUpdate = false
+			return true
+		}
 	}
 
-	return lp.charging() || honourUpdateInterval && (remaining <= 0) || lp.connected() && lp.socUpdated.IsZero()
+	return false
 }
 
 // checks if the connected charger can provide Soc to the connected vehicle
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/loadpoint_phases.go b/core/loadpoint_phases.go
index 346513ae..9cee4218 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -103,9 +103,6 @@ func (lp *Loadpoint) maxActivePhases() int {
 	// if 1p3p supported then assume configured limit or 3p
 	if _, ok := lp.charger.(api.PhaseSwitcher); ok {
 		physical = lp.ConfiguredPhases
-		if physical == 0 {
-			physical = 3
-		}
 	}
 
 	return min(expect(vehicle), expect(physical), expect(measured))
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 280929f3..a346c7be 100755
--- a/docker/bin/entrypoint.sh
+++ b/docker/bin/entrypoint.sh
@@ -7,16 +7,16 @@ HASSIO_OPTIONSFILE=/data/options.json
 if [ -f ${HASSIO_OPTIONSFILE} ]; then
     CONFIG=$(grep -o '"config_file": "[^"]*' ${HASSIO_OPTIONSFILE} | grep -o '[^"]*$')
     echo "Using config file: ${CONFIG}"
-
+    
     SQLITE_FILE=$(grep -o '"sqlite_file": "[^"]*' ${HASSIO_OPTIONSFILE} | grep -o '[^"]*$')
-
+    
     if [ ! -f "${CONFIG}" ]; then
         echo "Config not found. Please create a config under ${CONFIG}."
         echo "For details see evcc documentation at https://github.com/evcc-io/evcc#readme."
     else
         if [ "${SQLITE_FILE}" ]; then
-            echo "starting evcc: 'evcc --config ${CONFIG} --sqlite ${SQLITE_FILE}'"
-            exec evcc --config "${CONFIG}" --sqlite "${SQLITE_FILE}"
+            echo "starting evcc: '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/go.mod b/go.mod
index 03807f8b..d24bc837 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/andig/mbserver v0.0.0-20221101171752-364e71b0089c
 	github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
 	github.com/avast/retry-go/v3 v3.1.1
-	github.com/aws/aws-sdk-go v1.44.198
+	github.com/aws/aws-sdk-go v1.44.203
 	github.com/basgys/goxml2json v1.1.0
 	github.com/basvdlei/gotsmart v0.0.3
 	github.com/benbjohnson/clock v1.3.0
@@ -31,11 +31,11 @@ require (
 	github.com/enbility/eebus-go v0.1.6
 	github.com/fatih/structs v1.1.0
 	github.com/foogod/go-powerwall v0.2.0
-	github.com/glebarez/sqlite v1.6.0
+	github.com/glebarez/sqlite v1.7.0
 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
 	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
 	github.com/godbus/dbus/v5 v5.1.0
-	github.com/gokrazy/updater v0.0.0-20230206220435-64f39da38fa6
+	github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2
 	github.com/golang-module/carbon/v2 v2.2.3
 	github.com/golang/mock v1.6.0
 	github.com/google/go-github/v32 v32.1.0
@@ -46,7 +46,7 @@ require (
 	github.com/gregdel/pushover v1.1.0
 	github.com/grid-x/modbus v0.0.0-20230105075906-8b31e0d1647e
 	github.com/hashicorp/go-version v1.6.0
-	github.com/hasura/go-graphql-client v0.8.2-0.20230118111322-12655772463c
+	github.com/hasura/go-graphql-client v0.9.0
 	github.com/imdario/mergo v0.3.13
 	github.com/influxdata/influxdb-client-go/v2 v2.12.2
 	github.com/itchyny/gojq v0.12.11
@@ -90,12 +90,12 @@ require (
 	github.com/volkszaehler/mbmd v0.0.0-20230202102845-c58e07a6f048
 	github.com/writeas/go-strip-markdown v2.0.1+incompatible
 	gitlab.com/bboehmke/sunny v0.15.1-0.20211022160056-2fba1c86ade6
-	golang.org/x/exp v0.0.0-20230206171751-46f607a40771
-	golang.org/x/net v0.6.0
+	golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
+	golang.org/x/net v0.7.0
 	golang.org/x/oauth2 v0.5.0
 	golang.org/x/sync v0.1.0
 	golang.org/x/text v0.7.0
-	google.golang.org/api v0.109.0
+	google.golang.org/api v0.110.0
 	google.golang.org/grpc v1.53.0
 	google.golang.org/protobuf v1.28.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -130,7 +130,7 @@ require (
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/googleapis/enterprise-certificate-proxy v0.2.2 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/googleapis/gax-go/v2 v2.7.0 // indirect
 	github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
@@ -184,7 +184,7 @@ require (
 	golang.org/x/term v0.5.0 // indirect
 	golang.org/x/tools v0.6.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect
+	google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect
 	gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/sourcemap.v1 v1.0.5 // indirect
diff --git a/go.sum b/go.sum
index cd841f14..350021ca 100644
--- a/go.sum
+++ b/go.sum
@@ -247,8 +247,8 @@ github.com/avast/retry-go/v3 v3.1.1 h1:49Scxf4v8PmiQ/nY0aY3p0hDueqSmc7++cBbtiDGu
 github.com/avast/retry-go/v3 v3.1.1/go.mod h1:6cXRK369RpzFL3UQGqIUp9Q7GDrams+KsYWrfNA1/nQ=
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go v1.44.198 h1:kgnvxQv4/kP5M0nbxBx0Ac0so9ndr9f8Ti0g+NmPQF8=
-github.com/aws/aws-sdk-go v1.44.198/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.203 h1:pcsP805b9acL3wUqa4JR2vg1k2wnItkDYNvfmcy6F+U=
+github.com/aws/aws-sdk-go v1.44.203/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw=
 github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw=
@@ -279,15 +279,12 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
 github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
 github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
 github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
@@ -389,11 +386,10 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
-github.com/glebarez/go-sqlite v1.20.0/go.mod h1:uTnJoqtwMQjlULmljLT73Cg7HB+2X6evsBHODyyq1ak=
 github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
 github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
-github.com/glebarez/sqlite v1.6.0 h1:ZpvDLv4zBi2cuuQPitRiVz/5Uh6sXa5d8eBu0xNTpAo=
-github.com/glebarez/sqlite v1.6.0/go.mod h1:6D6zPU/HTrFlYmVDKqBJlmQvma90P6r7sRRdkUUZOYk=
+github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
+github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -455,8 +451,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/gokrazy/updater v0.0.0-20230206220435-64f39da38fa6 h1:4oHWkkyHw9NN0ZsfuGKkjw+SADUKiQmLYHy/U8G9MFg=
-github.com/gokrazy/updater v0.0.0-20230206220435-64f39da38fa6/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
+github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 h1:kBY5R1tSf+EYZ+QaSrofLaVJtBqYsVNVBWkdMq3Smcg=
+github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
 github.com/golang-module/carbon/v2 v2.2.3 h1:WvGIc5+qzq9drNzH+Gnjh1TZ0JgDY/IA+m2Dvk7Qm4Q=
 github.com/golang-module/carbon/v2 v2.2.3/go.mod h1:LdzRApgmDT/wt0eNT8MEJbHfJdSqCtT46uZhfF30dqI=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -541,7 +537,6 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
-github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -551,8 +546,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
 github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
 github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
 github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
-github.com/googleapis/enterprise-certificate-proxy v0.2.2 h1:jUqbmxlR+gGPQq/uvQviKpS1bSQecfs2t7o6F14sk9s=
-github.com/googleapis/enterprise-certificate-proxy v0.2.2/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -642,8 +637,8 @@ github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
 github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
-github.com/hasura/go-graphql-client v0.8.2-0.20230118111322-12655772463c h1:BruXcowJs5r1MKf534ObCBjA9zk5XKUZ5W2m4NdE3Ak=
-github.com/hasura/go-graphql-client v0.8.2-0.20230118111322-12655772463c/go.mod h1:NVifIwv+YFIUYGLQ7SM2/vBbzS/9rFP4vmIf/vf/zXM=
+github.com/hasura/go-graphql-client v0.9.0 h1:IPL+8CjLrTw4lAJno6qP6e9Nz64+fHSwJTbx4feXI80=
+github.com/hasura/go-graphql-client v0.9.0/go.mod h1:jlTUQWgTJOI4opQssMlCkzVjpRVSyL+PQsTjkAwcr3w=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
 github.com/holoplot/go-avahi v1.0.1 h1:XcqR2keL4qWRnlxHD5CAOdWpLFZJ+EOUK0vEuylfvvk=
@@ -655,7 +650,6 @@ github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
 github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
@@ -781,7 +775,6 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
 github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -1139,8 +1132,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg=
-golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
+golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1244,8 +1237,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1478,7 +1471,6 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
 golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -1555,8 +1547,8 @@ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ
 google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
 google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
 google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
-google.golang.org/api v0.109.0 h1:sW9hgHyX497PP5//NUM7nqfV8D0iDfBApqq7sOh1XR8=
-google.golang.org/api v0.109.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
+google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
+google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1670,8 +1662,8 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw
 google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
 google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
 google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
-google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
-google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
+google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@@ -1771,7 +1763,6 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
 gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
 gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1782,40 +1773,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
-lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
-modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
-modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
-modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
-modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
-modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
-modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
-modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
-modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
-modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
-modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
-modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
-modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
-modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
-modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
-modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
 modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
 modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
 modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
 modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
-modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
 modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
 modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
-modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
-modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
 modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
 modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
-modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
-modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
-modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
 nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
 nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
 pgregory.net/rapid v0.4.7 h1:MTNRktPuv5FNqOO151TM9mDTa+XHcX6ypYeISDVD14g=
diff --git a/i18n/lt.toml b/i18n/lt.toml
index c381ab33..59c7487d 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -143,7 +143,7 @@ planPeriodLabel = "Periodas"
 planPeriodValue = "{start} iki {end}"
 planUnknown = "dar nežinome"
 remove = "Panaikinti"
-setTargetTime = "nenustatytas"
+setTargetTime = "nesuplanuotas"
 targetIsInThePast = "Pasirinkite laiką ateityje."
 targetIsTooFarInTheFuture = "Pakoreguosime planą, kai tik gausime naujų duomenų."
 title = "Planinis įkrovimas"
diff --git a/packaging/init/evcc.service b/packaging/init/evcc.service
index 2b957fde..8cd7735e 100644
--- a/packaging/init/evcc.service
+++ b/packaging/init/evcc.service
@@ -10,7 +10,8 @@ StartLimitIntervalSec=10
 StartLimitBurst=10
 
 [Service]
-ExecStart=/usr/bin/evcc --sqlite /var/lib/evcc/evcc.db
+ExecStart=/usr/bin/evcc
+Environment="EVCC_DATABASE_DSN=/var/lib/evcc/evcc.db"
 Restart=always
 RestartSec=10
 
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/push/hub.go b/push/hub.go
index 2bff7dae..07c8697d 100644
--- a/push/hub.go
+++ b/push/hub.go
@@ -1,9 +1,9 @@
 package push
 
 import (
+	"fmt"
 	"strings"
 	"text/template"
-	"time"
 
 	"github.com/Masterminds/sprig/v3"
 	"github.com/evcc-io/evcc/util"
@@ -20,41 +20,27 @@ type EventTemplateConfig struct {
 	Title, Msg string
 }
 
-// EventTemplate is the push message template for an event
-type EventTemplate struct {
-	Title, Msg *template.Template
-}
-
 // Hub subscribes to event notifications and sends them to client devices
 type Hub struct {
-	definitions map[string]EventTemplate
+	definitions map[string]EventTemplateConfig
 	sender      []Messenger
 	cache       *util.Cache
 }
 
 // NewHub creates push hub with definitions and receiver
 func NewHub(cc map[string]EventTemplateConfig, cache *util.Cache) (*Hub, error) {
-	definitions := make(map[string]EventTemplate)
-
 	// instantiate all event templates
 	for k, v := range cc {
-		var def EventTemplate
-		var err error
-
-		def.Title, err = template.New("out").Funcs(template.FuncMap(sprig.FuncMap())).Parse(v.Title)
-		if err == nil {
-			def.Msg, err = template.New("out").Funcs(template.FuncMap(sprig.FuncMap())).Parse(v.Msg)
+		if _, err := template.New("out").Funcs(template.FuncMap(sprig.FuncMap())).Parse(v.Title); err != nil {
+			return nil, fmt.Errorf("invalid event title: %s (%w)", k, err)
 		}
-
-		if err != nil {
-			return nil, err
+		if _, err := template.New("out").Funcs(template.FuncMap(sprig.FuncMap())).Parse(v.Msg); err != nil {
+			return nil, fmt.Errorf("invalid event message: %s (%w)", k, err)
 		}
-
-		definitions[k] = def
 	}
 
 	h := &Hub{
-		definitions: definitions,
+		definitions: cc,
 		cache:       cache,
 	}
 
@@ -67,12 +53,9 @@ func (h *Hub) Add(sender Messenger) {
 }
 
 // apply applies the event template to the content to produce the actual message
-func (h *Hub) apply(ev Event, tmpl *template.Template) (string, error) {
+func (h *Hub) apply(ev Event, tmpl string) (string, error) {
 	attr := make(map[string]interface{})
 
-	// let cache catch up, refs reverted https://github.com/evcc-io/evcc/pull/445
-	time.Sleep(100 * time.Millisecond)
-
 	// loadpoint id
 	if ev.Loadpoint != nil {
 		attr["loadpoint"] = *ev.Loadpoint + 1
@@ -85,17 +68,11 @@ func (h *Hub) apply(ev Event, tmpl *template.Template) (string, error) {
 		}
 	}
 
-	// apply data attributes to template using sprig functions
-	applied := new(strings.Builder)
-	if err := tmpl.Execute(applied, attr); err != nil {
-		return "", err
-	}
-
-	return util.ReplaceFormatted(applied.String(), attr)
+	return util.ReplaceFormatted(tmpl, attr)
 }
 
 // Run is the Hub's main publishing loop
-func (h *Hub) Run(events <-chan Event) {
+func (h *Hub) Run(events <-chan Event, valueChan chan util.Param) {
 	log := util.NewLogger("push")
 
 	for ev := range events {
@@ -108,6 +85,11 @@ func (h *Hub) Run(events <-chan Event) {
 			continue
 		}
 
+		// let cache catch up, refs https://github.com/evcc-io/evcc/pull/445
+		flushC := util.Flusher()
+		valueChan <- util.Param{Val: flushC}
+		<-flushC
+
 		title, err := h.apply(ev, definition.Title)
 		if err != nil {
 			log.ERROR.Printf("invalid title template for %s: %v", ev.Event, err)
diff --git a/push/pushover.go b/push/pushover.go
index 09832b7c..502e0b1d 100644
--- a/push/pushover.go
+++ b/push/pushover.go
@@ -26,7 +26,6 @@ func NewPushOverFromConfig(other map[string]interface{}) (Messenger, error) {
 		App        string
 		Recipients []string
 		Devices    []string
-		Events     map[string]EventTemplate
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
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"
           },
diff --git a/templates/definition/charger/smaevcharger.yaml b/templates/definition/charger/smaevcharger.yaml
index b1a0cb83..70a411b2 100644
--- a/templates/definition/charger/smaevcharger.yaml
+++ b/templates/definition/charger/smaevcharger.yaml
@@ -7,8 +7,8 @@ capabilities: ["mA"]
 requirements:
   evcc: ["sponsorship"]
   description:
-    de: Der EV Charger muss sich im Modus "Fast" befinden.
-    en: The charger must be switched to "Fast" charging mode.
+    de: Der EV Charger muss sich im Modus "Fast" befinden und der Benutzer muss die Rechte "Administrator" haben.
+    en: The charger must be switched to "Fast" charging mode and the user must have "Administrator" rights.
 params:
   - name: host
     required: true
diff --git a/templates/definition/meter/e3dc.yaml b/templates/definition/meter/e3dc.yaml
index 1df2aace..8aaabdbe 100644
--- a/templates/definition/meter/e3dc.yaml
+++ b/templates/definition/meter/e3dc.yaml
@@ -12,26 +12,46 @@ params:
     advanced: true
 render: |
   type: custom
+  {{- if eq .usage "grid" }}
   power:
     source: modbus
     uri: {{ .host }}:{{ .port }}
-    register: # manual register configuration for E3/DC "Simple-Mode"
+    register:
       type: holding
       decode: int32s
-  {{- if eq .usage "grid" }}
-      address: 40073 # Hausverbrauchs-Leistung in Watt
+      address: 40073 # Hausverbrauchsleistung in Watt
   {{- end}}
   {{- if eq .usage "pv" }}
-      address: 40067 # Photovoltaikleistung in Watt
+  power:
+    source: calc
+    add:
+    - source: modbus
+      uri: {{ .host }}:{{ .port }}
+      register:
+        type: holding
+        decode: int32s
+        address: 40067 # Photovoltaikleistung in Watt
+    - source: modbus
+      uri: {{ .host }}:{{ .port }}
+      register:
+        type: holding
+        decode: int32s
+        address: 40075 # Leistung zusätzlicher Einspeiser in Watt
   {{- end}}
   {{- if eq .usage "battery" }}
-      address: 40069 # Batterie-Leistung in Watt
+  power:
+    source: modbus
+    uri: {{ .host }}:{{ .port }}
+    register:
+      type: holding
+      decode: int32s
+      address: 40069 # Batterieleistung in Watt
     scale: -1 # reverse direction
   soc:
     source: modbus
     uri: {{ .host }}:{{ .port }}
-    register: # manual register configuration for E3/DC "Simple-Mode"
-      address: 40082 # Batterie-SOC in Prozent
+    register:
+      address: 40082 # Batterie-SOC in %
       type: holding
       decode: uint16
   {{- if ne .capacity "" }}
diff --git a/templates/docs/charger/smaevcharger_0.yaml b/templates/docs/charger/smaevcharger_0.yaml
index e18c566c..65ac0c9b 100644
--- a/templates/docs/charger/smaevcharger_0.yaml
+++ b/templates/docs/charger/smaevcharger_0.yaml
@@ -4,7 +4,7 @@ product:
 capabilities: ["mA"]
 requirements: ["sponsorship"]
 description: |
-  Der EV Charger muss sich im Modus "Fast" befinden.
+  Der EV Charger muss sich im Modus "Fast" befinden und der Benutzer muss die Rechte "Administrator" haben.
 render:
   - default: |
       type: template
diff --git a/util/cache.go b/util/cache.go
index 6435bd02..2818d4c3 100644
--- a/util/cache.go
+++ b/util/cache.go
@@ -11,6 +11,16 @@ type Cache struct {
 	val map[string]Param
 }
 
+// flush is the value type used as parameter for flushing the cache.
+// Flushing is implemented by closing the channel. At this time, it is guaranteed
+// that the cache has catched up processing all pending messages.
+type flush chan struct{}
+
+// Flusher returns a new flush channel
+func Flusher() flush {
+	return make(flush)
+}
+
 // NewCache creates cache
 func NewCache() *Cache {
 	return &Cache{
@@ -23,10 +33,16 @@ func (c *Cache) Run(in <-chan Param) {
 	log := NewLogger("cache")
 
 	for p := range in {
+		if flushC, ok := p.Val.(flush); ok {
+			close(flushC)
+			continue
+		}
+
 		key := p.Key
 		if p.Loadpoint != nil {
 			key = fmt.Sprintf("lp-%d/%s", *p.Loadpoint+1, key)
 		}
+
 		log.TRACE.Printf("%s: %v", key, p.Val)
 		c.Add(p.UniqueID(), p)
 	}
diff --git a/util/format.go b/util/format.go
index e8149762..abcfea83 100644
--- a/util/format.go
+++ b/util/format.go
@@ -61,8 +61,7 @@ func ReplaceFormatted(s string, kv map[string]interface{}) (string, error) {
 	}
 
 	var rs bytes.Buffer
-	err = tpl.Execute(&rs, kv)
-	if err != nil {
+	if err := tpl.Execute(&rs, kv); err != nil {
 		return s, err
 	}
 	s = rs.String()


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index f8c15d3e..1345f718 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -680,7 +680,10 @@ func TestSocPoll(t *testing.T) {
 		{pollCharging, api.StatusC, -1, true},
 		{pollCharging, api.StatusC, 0, true},
 		{pollCharging, api.StatusC, tNoRefresh, true}, // cached by vehicle
-		{pollCharging, api.StatusC, tRefresh, true},
+		{pollCharging, api.StatusC, tRefresh, true},   // will set lp.didChargeOnLastSocUpdate
+		{pollCharging, api.StatusB, 0, false},         // last update must wait for interval
+		{pollCharging, api.StatusB, tRefresh, true},   // update once if connected and was charging
+		{pollCharging, api.StatusB, tRefresh, false},  // but only once
 
 		// pollConnected
 		{pollConnected, api.StatusA, -1, false},
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)
+	}
+}
diff --git a/util/format_test.go b/util/format_test.go
index dcb5e3bf..7cc23bcd 100644
--- a/util/format_test.go
+++ b/util/format_test.go
@@ -38,6 +38,8 @@ func TestReplace(t *testing.T) {
 		{"foo", true, "${foo}", "true"},
 		{"foo", "1", "abc${foo}${foo}", "abc11"},
 		{"foo", math.Pi, "${foo:%.2f}", "3.14"},
+		{"foo", math.Pi, "${foo:%.0f}%", "3%"},
+		{"foo", 3, "${foo}%", "3%"},
 	}
 
 	for _, c := range cases {