Repository: evcc-io/evcc
Pull request #6252 (2023-02-15 13:00:48)

Add exponential backoff retries for failed cache refresh
This PR adds the ability to retry failed cache refreshes with exponential backoff. Backoff wait time will increase by 2^n minutes to a maximum of the regular cache time.
This way, an API error will not necessarily be cached the whole regular cache time. At the same time, overloading the API with too many refresh attempts is avoided.

For the default of 15 minutes cache time, the retries will happen after 1m, 2m, 4m, 8m and 15m (15 instead of 16 because the backoff time cannot exceed the regular cache time).


andig: The diff is much larger than I thought. Do we really need the extra param? There is also https://pkg.go.dev/github.com/cenkalti/backoff/v4 which might simplify the backoff algo and is already user in other parts of the code.
tobiasbayer: I had a look at this lib too but deemed it to be too much overhead for the rather simple task we would like to achieve.
The extra param is meant for selectively using the backoff for well defined cases. We could do without but then it would be all or nothing.
The actual diff for the algo is about 50 loc (tests and extra param aside).
andig: If we don't have these cases we should minimize the impact. 
tobiasbayer: I took a look at the lib again. The problem is that it is not a good fit for the way we are calling the cache. 
We want to cache the error e.g. from the vehicle API for a short time. We do not want to try as often as the backoff allows until we either get a valid response or a backoff timeout. But that would be the behaviour of the `Retry` function from the lib.

Also, we have an external scheduler that calls the getter typically every 10 seconds. Introducing the `Retry` function from the lib would mean, that inside this cache getter, new threads would be started. We would have to do all sorts of thread synchronization ourselves.

Patch:
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.story.vue b/assets/js/components/Energyflow/Energyflow.story.vue
index c8b30c82..5d436d6b 100644
--- a/assets/js/components/Energyflow/Energyflow.story.vue
+++ b/assets/js/components/Energyflow/Energyflow.story.vue
@@ -14,6 +14,7 @@ import Energyflow from "./Energyflow.vue";
 				:loadpointsPower="4200"
 				:activeLoadpointsCount="3"
 				siteTitle="Home"
+				:pv="[{ power: 5000 }, { power: 2300 }]"
 			/>
 		</Variant>
 		<Variant title="battery and grid">
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index 8dce4c92..9382bd35 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -16,7 +16,7 @@
 				:pvProduction="pvProduction"
 				:homePower="homePower"
 				:batterySoc="batterySoc"
-				:valuesInKw="valuesInKw"
+				:powerInKw="powerInKw"
 				:vehicleIcons="vehicleIcons"
 			/>
 		</div>
@@ -56,26 +56,28 @@
 							:name="$t('main.energyflow.pvProduction')"
 							icon="sun"
 							:power="pvProduction"
-							:valuesInKw="valuesInKw"
+							:powerTooltip="pvTooltip"
+							:powerInKw="powerInKw"
 						/>
 						<EnergyflowEntry
 							v-if="batteryConfigured"
 							:name="$t('main.energyflow.batteryDischarge')"
 							icon="battery"
-							:soc="batterySoc"
 							:power="batteryDischarge"
-							:valuesInKw="valuesInKw"
-							:tooltip="batteryTooltip"
+							:powerInKw="powerInKw"
+							:soc="batterySoc"
+							:details="batterySoc"
+							:detailsFmt="batteryFmt"
+							:detailsTooltip="batteryTooltip"
 						/>
 						<EnergyflowEntry
 							:name="$t('main.energyflow.gridImport')"
 							icon="powersupply"
 							:power="gridImport"
-							:valuesInKw="valuesInKw"
-							:price="tariffGrid"
-							:currency="currency"
-							:co2="tariffCo2"
-							:tooltip="detailsTooltip(tariffGrid, tariffCo2)"
+							:powerInKw="powerInKw"
+							:details="detailsValue(tariffGrid, tariffCo2)"
+							:detailsFmt="detailsFmt"
+							:detailsTooltip="detailsTooltip(tariffGrid, tariffCo2)"
 						/>
 					</div>
 				</div>
@@ -93,11 +95,12 @@
 							:name="$t('main.energyflow.homePower')"
 							icon="home"
 							:power="homePower"
-							:valuesInKw="valuesInKw"
-							:price="tariffEffectivePrice"
-							:currency="currency"
-							:co2="tariffEffectiveCo2"
-							:tooltip="detailsTooltip(tariffEffectivePrice, tariffEffectiveCo2)"
+							:powerInKw="powerInKw"
+							:details="detailsValue(tariffEffectivePrice, tariffEffectiveCo2)"
+							:detailsFmt="detailsFmt"
+							:detailsTooltip="
+								detailsTooltip(tariffEffectivePrice, tariffEffectiveCo2)
+							"
 						/>
 						<EnergyflowEntry
 							:name="
@@ -108,29 +111,36 @@
 							icon="vehicle"
 							:vehicleIcons="vehicleIcons"
 							:power="loadpointsPower"
-							:valuesInKw="valuesInKw"
-							:price="tariffEffectivePrice"
-							:currency="currency"
-							:co2="tariffEffectiveCo2"
-							:tooltip="detailsTooltip(tariffEffectivePrice, tariffEffectiveCo2)"
+							:powerInKw="powerInKw"
+							:details="
+								activeLoadpointsCount
+									? detailsValue(tariffEffectivePrice, tariffEffectiveCo2)
+									: undefined
+							"
+							:detailsFmt="detailsFmt"
+							:detailsTooltip="
+								detailsTooltip(tariffEffectivePrice, tariffEffectiveCo2)
+							"
 						/>
 						<EnergyflowEntry
 							v-if="batteryConfigured"
 							:name="$t('main.energyflow.batteryCharge')"
 							icon="battery"
-							:soc="batterySoc"
 							:power="batteryCharge"
-							:valuesInKw="valuesInKw"
-							:tooltip="batteryTooltip"
+							:powerInKw="powerInKw"
+							:soc="batterySoc"
+							:details="batterySoc"
+							:detailsFmt="batteryFmt"
+							:detailsTooltip="batteryTooltip"
 						/>
 						<EnergyflowEntry
 							:name="$t('main.energyflow.pvExport')"
 							icon="powersupply"
 							:power="pvExport"
-							:valuesInKw="valuesInKw"
-							:price="-tariffFeedIn"
-							:currency="currency"
-							:tooltip="detailsTooltip(-tariffFeedIn)"
+							:powerInKw="powerInKw"
+							:details="detailsValue(-tariffFeedIn)"
+							:detailsFmt="detailsFmt"
+							:detailsTooltip="detailsTooltip(-tariffFeedIn)"
 						/>
 					</div>
 				</div>
@@ -146,6 +156,7 @@ import EnergyflowEntry from "./EnergyflowEntry.vue";
 import formatter from "../../mixins/formatter";
 import AnimatedNumber from "../AnimatedNumber.vue";
 import settings from "../../settings";
+import { showGridPrice, showGridCo2 } from "../../gridDetails";
 
 export default {
 	name: "Energyflow",
@@ -156,6 +167,7 @@ export default {
 		gridPower: { type: Number, default: 0 },
 		homePower: { type: Number, default: 0 },
 		pvConfigured: Boolean,
+		pv: { type: Array },
 		pvPower: { type: Number, default: 0 },
 		loadpointsPower: { type: Number, default: 0 },
 		activeLoadpointsCount: { type: Number, default: 0 },
@@ -199,7 +211,7 @@ export default {
 		pvExport: function () {
 			return Math.max(0, this.gridPower * -1);
 		},
-		valuesInKw: function () {
+		powerInKw: function () {
 			return this.gridImport + this.selfConsumption + this.pvExport > 1000;
 		},
 		inPower: function () {
@@ -211,6 +223,12 @@ export default {
 		detailsHeight: function () {
 			return this.detailsOpen ? this.detailsCompleteHeight + "px" : 0;
 		},
+		pvTooltip() {
+			if (!Array.isArray(this.pv) || this.pv.length <= 1) {
+				return;
+			}
+			return this.pv.map(({ power }) => this.fmtKw(power, this.powerInKw));
+		},
 		batteryTooltip() {
 			if (!Array.isArray(this.battery)) {
 				return;
@@ -221,6 +239,9 @@ export default {
 				return this.$t("main.energyflow.batteryTooltip", { energy, total, soc });
 			});
 		},
+		batteryFmt() {
+			return (soc) => `${Math.round(soc)}%`;
+		},
 	},
 	mounted() {
 		window.addEventListener("resize", this.updateHeight);
@@ -243,8 +264,22 @@ export default {
 			}
 			return result;
 		},
+		detailsValue(price, co2) {
+			if (showGridPrice() && !isNaN(price)) {
+				return price;
+			}
+			if (showGridCo2() && !isNaN(co2)) {
+				return co2;
+			}
+		},
+		detailsFmt(value) {
+			if (showGridPrice()) {
+				return this.fmtPricePerKWh(value, this.currency, true);
+			}
+			return this.fmtCo2Short(value);
+		},
 		kw: function (watt) {
-			return this.fmtKw(watt, this.valuesInKw);
+			return this.fmtKw(watt, this.powerInKw);
 		},
 		toggleDetails: function () {
 			this.updateHeight();
diff --git a/assets/js/components/Energyflow/EnergyflowEntry.vue b/assets/js/components/Energyflow/EnergyflowEntry.vue
index 56a485a8..8e6c339b 100644
--- a/assets/js/components/Energyflow/EnergyflowEntry.vue
+++ b/assets/js/components/Energyflow/EnergyflowEntry.vue
@@ -10,11 +10,11 @@
 		</span>
 		<span class="text-end text-nowrap ps-1 fw-bold d-flex">
 			<div ref="details" class="fw-normal" data-bs-toggle="tooltip" title=" " @click.stop="">
-				<span v-if="showPrice()"><AnimatedNumber :to="price" :format="fmtPrice" /></span>
-				<span v-if="showCo2()"><AnimatedNumber :to="co2" :format="fmtCo2Short" /></span>
-				<span v-if="hasSoc">{{ soc }}%</span>
+				<AnimatedNumber v-if="details !== undefined" :to="details" :format="detailsFmt" />
+			</div>
+			<div ref="power" class="power" data-bs-toggle="tooltip" title=" " @click.stop="">
+				<AnimatedNumber :to="power" :format="kw" />
 			</div>
-			<AnimatedNumber :to="power" :format="kw" class="power" />
 		</span>
 	</div>
 </template>
@@ -28,7 +28,6 @@ import BatteryIcon from "./BatteryIcon.vue";
 import formatter from "../../mixins/formatter";
 import AnimatedNumber from "../AnimatedNumber.vue";
 import VehicleIcon from "../VehicleIcon";
-import { showGridPrice, showGridCo2 } from "../../gridDetails";
 
 export default {
 	name: "EnergyflowEntry",
@@ -37,17 +36,17 @@ export default {
 	props: {
 		name: { type: String },
 		icon: { type: String },
+		vehicleIcons: { type: Array },
 		power: { type: Number },
+		powerTooltip: { type: Array },
+		powerInKw: { type: Boolean },
 		soc: { type: Number },
-		price: { type: Number },
-		co2: { type: Number },
-		valuesInKw: { type: Boolean },
-		vehicleIcons: { type: Array },
-		currency: { type: String },
-		tooltip: { type: Array },
+		details: { type: Number },
+		detailsFmt: { type: Function },
+		detailsTooltip: { type: Array },
 	},
 	data() {
-		return { tooltipInstance: null };
+		return { powerTooltipInstance: null, detailsTooltipInstance: null };
 	},
 	computed: {
 		active: function () {
@@ -59,42 +58,54 @@ export default {
 		isVehicle: function () {
 			return this.icon === "vehicle";
 		},
-		hasSoc: function () {
-			return this.isBattery && !isNaN(this.soc);
-		},
 	},
 	watch: {
-		tooltip(newVal, oldVal) {
+		powerTooltip(newVal, oldVal) {
+			if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
+				this.updatePowerTooltip();
+			}
+		},
+		detailsTooltip(newVal, oldVal) {
 			if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
-				this.updateTooltip();
+				this.updateDetailsTooltip();
 			}
 		},
 	},
 	mounted: function () {
-		this.updateTooltip();
+		this.updatePowerTooltip();
+		this.updateDetailsTooltip();
 	},
 	methods: {
-		showPrice() {
-			return showGridPrice() && !isNaN(this.price);
-		},
-		showCo2() {
-			return showGridCo2() && !isNaN(this.co2);
-		},
 		kw: function (watt) {
-			return this.fmtKw(watt, this.valuesInKw);
+			return this.fmtKw(watt, this.powerInKw);
+		},
+		updatePowerTooltip() {
+			this.powerTooltipInstance = this.updateTooltip(
+				this.powerTooltipInstance,
+				this.powerTooltip,
+				this.$refs.power
+			);
 		},
-		fmtPrice: function (price) {
-			return this.fmtPricePerKWh(price, this.currency, true);
+		updateDetailsTooltip() {
+			this.detailsTooltipInstance = this.updateTooltip(
+				this.detailsTooltipInstance,
+				this.detailsTooltip,
+				this.$refs.details
+			);
 		},
-		updateTooltip: function () {
-			if (!Array.isArray(this.tooltip) || !this.tooltip.length) {
+		updateTooltip: function (instance, content, ref) {
+			if (!Array.isArray(content) || !content.length) {
+				if (instance) {
+					instance.dispose();
+				}
 				return;
 			}
-			if (!this.tooltipInstance) {
-				this.tooltipInstance = new Tooltip(this.$refs.details, { html: true });
+			if (!instance) {
+				instance = new Tooltip(ref, { html: true });
 			}
-			const html = `<div class="text-end">${this.tooltip.join("<br/>")}</div>`;
-			this.tooltipInstance.setContent({ ".tooltip-inner": html });
+			const html = `<div class="text-end">${content.join("<br/>")}</div>`;
+			instance.setContent({ ".tooltip-inner": html });
+			return instance;
 		},
 	},
 };
diff --git a/assets/js/components/Energyflow/Visualization.vue b/assets/js/components/Energyflow/Visualization.vue
index b9a8120d..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"
@@ -98,11 +98,11 @@ export default {
 		pvProduction: { type: Number, default: 0 },
 		homePower: { type: Number, default: 0 },
 		batterySoc: { type: Number, default: 0 },
-		valuesInKw: { type: Boolean, default: false },
+		powerInKw: { type: Boolean, default: false },
 		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.valuesInKw, 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/Site.vue b/assets/js/components/Site.vue
index b0587213..c039640d 100644
--- a/assets/js/components/Site.vue
+++ b/assets/js/components/Site.vue
@@ -58,6 +58,7 @@ export default {
 		homePower: Number,
 		pvConfigured: Boolean,
 		pvPower: Number,
+		pv: Array,
 		batteryConfigured: Boolean,
 		batteryPower: Number,
 		batterySoc: Number,
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..c4ed0023 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -57,9 +57,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,6 +86,7 @@ func initConfig() {
 	}
 
 	viper.SetEnvPrefix("evcc")
+	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
 	viper.AutomaticEnv() // read in environment variables that match
 
 	// print version
@@ -207,7 +205,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_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/docker/bin/entrypoint.sh b/docker/bin/entrypoint.sh
index 280929f3..8f537ec0 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 EVCC_DATABASE_DSN="${SQLITE_FILE}" evcc --config "${CONFIG}"
         else
             echo "starting evcc: 'evcc --config ${CONFIG}'"
             exec evcc --config "${CONFIG}"
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/templates/definition/charger/ocpp.yaml b/templates/definition/charger/ocpp.yaml
index 26b7807b..955201ac 100644
--- a/templates/definition/charger/ocpp.yaml
+++ b/templates/definition/charger/ocpp.yaml
@@ -63,9 +63,7 @@ params:
       de: Token-ID welche für die Freischaltung der Ladevorgänge an den Ladepunkt zurückgesendet wird
     example: 04E6B78921BBA0
   - name: timeout
-    advanced: true
     default: 10m
-    type: duration
 render: |
   type: ocpp
   {{- if ne .stationid "" }}
diff --git a/templates/definition/charger/pracht-alpha.yaml b/templates/definition/charger/pracht-alpha.yaml
index 1b7bd1a7..5f4078cc 100644
--- a/templates/definition/charger/pracht-alpha.yaml
+++ b/templates/definition/charger/pracht-alpha.yaml
@@ -12,7 +12,9 @@ params:
     comset: 8N1
     id: 1
   - name: connector
+  - name: timeout
 render: |
   type: pracht-alpha
   {{- include "modbus" . }}
   connector: {{ .connector }}
+  timeout: {{ .timeout }}
diff --git a/templates/definition/charger/tinkerforge-warp.yaml b/templates/definition/charger/tinkerforge-warp.yaml
index 34a4e782..a6da4574 100644
--- a/templates/definition/charger/tinkerforge-warp.yaml
+++ b/templates/definition/charger/tinkerforge-warp.yaml
@@ -37,9 +37,7 @@ params:
   - name: topic
     default: warp
   - name: timeout
-    advanced: true
     default: 30s
-    type: duration
     help:
       de: Akzeptiere keine Daten die älter sind als dieser Wert
       en: Don't accept values older than this value
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/definition/meter/sax.yaml b/templates/definition/meter/sax.yaml
new file mode 100644
index 00000000..b2f09b44
--- /dev/null
+++ b/templates/definition/meter/sax.yaml
@@ -0,0 +1,41 @@
+template: sax
+products:
+  - brand: SAX
+    description:
+      generic: Homespeicher
+params:
+  - name: usage
+    choice: ["grid", "battery"]
+  - name: modbus
+    choice: ["tcpip"]
+    port: 3600
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    # register details
+    register:
+      address: 48 # Leistung des Smartmeters
+      type: holding
+      decode: int16
+  {{- end }}
+  {{- if eq .usage "battery" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    # register details
+    register:
+      address: 47 # Leistung P vom Speicher. (Messwert)
+      type: holding
+      decode: int16
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    # register details
+    register:
+      address: 46 # SOC vom Speicher (Messwert)
+      type: holding
+      decode: uint16
+  {{- end }}
diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml
index 8f831e26..d1ae3bea 100644
--- a/templates/definition/meter/solaredge-hybrid.yaml
+++ b/templates/definition/meter/solaredge-hybrid.yaml
@@ -16,7 +16,6 @@ params:
     id: 1
     port: 1502
   - name: timeout
-    type: duration
   - name: capacity
     advanced: true
 render: |
diff --git a/templates/definition/meter/solaredge-inverter.yaml b/templates/definition/meter/solaredge-inverter.yaml
index 0fb6abf7..95589c58 100644
--- a/templates/definition/meter/solaredge-inverter.yaml
+++ b/templates/definition/meter/solaredge-inverter.yaml
@@ -17,7 +17,6 @@ params:
     id: 1
     port: 1502
   - name: timeout
-    type: duration
 render: |
   type: custom
   power:
diff --git a/templates/docs/charger/pracht-alpha_0.yaml b/templates/docs/charger/pracht-alpha_0.yaml
index ad1f9030..88d6694f 100644
--- a/templates/docs/charger/pracht-alpha_0.yaml
+++ b/templates/docs/charger/pracht-alpha_0.yaml
@@ -48,3 +48,4 @@ render:
       host: 192.0.2.2 # Hostname
       port: 502 # Port
       connector: 1 # Optional
+      timeout: 10s # Optional
diff --git a/templates/docs/meter/sax_0.yaml b/templates/docs/meter/sax_0.yaml
new file mode 100644
index 00000000..7c159ec7
--- /dev/null
+++ b/templates/docs/meter/sax_0.yaml
@@ -0,0 +1,26 @@
+product:
+  brand: SAX
+  description: Homespeicher
+render:
+  - usage: grid
+    default: |
+      type: template
+      template: sax
+      usage: grid      
+
+      # Modbus TCP
+      modbus: tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 3600 # Port
+  - usage: battery
+    default: |
+      type: template
+      template: sax
+      usage: battery      
+
+      # Modbus TCP
+      modbus: tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 3600 # Port
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index a7639f39..50c95d4a 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -79,6 +79,7 @@
   "Powerfox",
   "Qcells",
   "RCT",
+  "SAX",
   "SENEC",
   "Shelly",
   "Siemens",
@@ -116,6 +117,7 @@
   "Powerfox",
   "Qcells",
   "RCT",
+  "SAX",
   "SENEC",
   "Senergy",
   "Shelly",
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/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 {