Repository: evcc-io/evcc
Pull request #12835 (2024-03-10 07:42:36)

UI: reload on upgrade and startup error fix
- 👹 switch from startup error screen to main ui, when yaml error was resolved after restart
- 💫 reload UI when evcc version changes -> no need for manual refresh in open browsers
- 🚨 html validator fix: broken label/id ref when loadpoint has no 1p3p


Patch:
diff --git a/README.md b/README.md
index ce434c30..563cfd5b 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe
 
 - simple and clean user interface
 - wide range of supported [chargers](https://docs.evcc.io/docs/devices/chargers):
-  - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Menneckes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more
+  - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more
   - experimental EEBus support (Elli, PMCC)
   - experimental OCPP support
   - Build-your-own: Phoenix Contact (includes ESL Walli), [EVSE DIN](http://evracing.cz/simple-evse-wallbox)
@@ -97,7 +97,7 @@ make ui build
 
 ### Code formatting
 
-We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Veture](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running:
+We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running:
 
 ```sh
 make lint
diff --git a/api/feature.go b/api/feature.go
index 1621b5d7..179ef0b5 100644
--- a/api/feature.go
+++ b/api/feature.go
@@ -17,4 +17,5 @@ const (
 	CoarseCurrent
 	IntegratedDevice
 	Heating
+	Retryable
 )
diff --git a/api/feature_enumer.go b/api/feature_enumer.go
index faaa1c60..8636adf6 100644
--- a/api/feature_enumer.go
+++ b/api/feature_enumer.go
@@ -7,11 +7,11 @@ import (
 	"strings"
 )
 
-const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeating"
+const _FeatureName = "OfflineCoarseCurrentIntegratedDeviceHeatingRetryable"
 
-var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43}
+var _FeatureIndex = [...]uint8{0, 7, 20, 36, 43, 52}
 
-const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheating"
+const _FeatureLowerName = "offlinecoarsecurrentintegrateddeviceheatingretryable"
 
 func (i Feature) String() string {
 	i -= 1
@@ -29,9 +29,10 @@ func _FeatureNoOp() {
 	_ = x[CoarseCurrent-(2)]
 	_ = x[IntegratedDevice-(3)]
 	_ = x[Heating-(4)]
+	_ = x[Retryable-(5)]
 }
 
-var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating}
+var _FeatureValues = []Feature{Offline, CoarseCurrent, IntegratedDevice, Heating, Retryable}
 
 var _FeatureNameToValueMap = map[string]Feature{
 	_FeatureName[0:7]:        Offline,
@@ -42,6 +43,8 @@ var _FeatureNameToValueMap = map[string]Feature{
 	_FeatureLowerName[20:36]: IntegratedDevice,
 	_FeatureName[36:43]:      Heating,
 	_FeatureLowerName[36:43]: Heating,
+	_FeatureName[43:52]:      Retryable,
+	_FeatureLowerName[43:52]: Retryable,
 }
 
 var _FeatureNames = []string{
@@ -49,6 +52,7 @@ var _FeatureNames = []string{
 	_FeatureName[7:20],
 	_FeatureName[20:36],
 	_FeatureName[36:43],
+	_FeatureName[43:52],
 }
 
 // FeatureString retrieves an enum value from the enum constants string name.
diff --git a/assets/css/app.css b/assets/css/app.css
index 1e923c6d..05c212c2 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -508,3 +508,10 @@ html.app .modal-dialog {
 	margin-top: env(safe-area-inset-top);
 	margin-bottom: env(safe-area-inset-bottom);
 }
+
+.btn-neutral {
+	all: unset;
+}
+.btn-neutral:hover {
+	outline: revert;
+}
diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue
index 1a785847..176d00f8 100644
--- a/assets/js/components/Loadpoint.vue
+++ b/assets/js/components/Loadpoint.vue
@@ -236,6 +236,11 @@ export default {
 		vehicleHasSoc: function () {
 			return this.vehicleKnown && !this.vehicle?.features?.includes("Offline");
 		},
+		vehicleNotReachable: function () {
+			// online vehicle that was not reachable at startup
+			const features = this.vehicle?.features || [];
+			return features.includes("Offline") && features.includes("Retryable");
+		},
 		socBasedCharging: function () {
 			return this.vehicleHasSoc || this.vehicleSoc > 0;
 		},
diff --git a/assets/js/components/LoadpointSettingsModal.vue b/assets/js/components/LoadpointSettingsModal.vue
index b6dfec56..2789a6eb 100644
--- a/assets/js/components/LoadpointSettingsModal.vue
+++ b/assets/js/components/LoadpointSettingsModal.vue
@@ -35,7 +35,7 @@
 							</h4>
 							<div v-if="phasesOptions.length" class="mb-3 row">
 								<label
-									:for="formId('phases_0')"
+									:for="formId(`phases_${phasesOptions[0]}`)"
 									class="col-sm-4 col-form-label pt-0"
 								>
 									{{ $t("main.loadpointSettings.phasesConfigured.label") }}
diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue
index 168c754a..5fd999b7 100644
--- a/assets/js/components/Loadpoints.vue
+++ b/assets/js/components/Loadpoints.vue
@@ -32,7 +32,15 @@
 				:class="{ 'indicator--selected': selected(index) }"
 				@click="scrollTo(index)"
 			>
-				<shopicon-filled-circle class="indicator-icon"></shopicon-filled-circle>
+				<shopicon-filled-lightning
+					v-if="isCharging(loadpoint)"
+					class="indicator-icon"
+				></shopicon-filled-lightning>
+				<shopicon-filled-circle
+					v-else-if="loadpoint.connected"
+					class="indicator-icon"
+				></shopicon-filled-circle>
+				<shopicon-bold-circle v-else class="indicator-icon"></shopicon-bold-circle>
 			</button>
 		</div>
 	</div>
@@ -40,6 +48,8 @@
 
 <script>
 import "@h2d2/shopicons/es/filled/circle";
+import "@h2d2/shopicons/es/bold/circle";
+import "@h2d2/shopicons/es/filled/lightning";
 
 import Loadpoint from "./Loadpoint.vue";
 
@@ -73,6 +83,9 @@ export default {
 			const { offsetWidth } = this.$refs.carousel.children[0];
 			this.selectedIndex = Math.round((scrollLeft - 7.5) / offsetWidth);
 		},
+		isCharging(lp) {
+			return lp.charging && lp.chargePower > 0;
+		},
 		selected(index) {
 			return this.selectedIndex === index;
 		},
diff --git a/assets/js/components/MaterialIcon/CloudOffline.vue b/assets/js/components/MaterialIcon/CloudOffline.vue
new file mode 100644
index 00000000..75dbc8a6
--- /dev/null
+++ b/assets/js/components/MaterialIcon/CloudOffline.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<path
+			fill="currentColor"
+			d="M6.5 20q-2.3 0-3.9-1.6T1 14.5q0-1.925 1.188-3.425T5.25 9.15q.075-.2.15-.387t.15-.413L2.1 4.9q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l17 17q.275.275.288.688t-.288.712q-.275.275-.687.288t-.713-.263L17.15 20zm0-2h8.65L7.1 9.95q-.05.275-.075.525T7 11h-.5q-1.45 0-2.475 1.025T3 14.5q0 1.45 1.025 2.475T6.5 18m15.1.75l-1.45-1.4q.425-.35.638-.812T21 15.5q0-1.05-.725-1.775T18.5 13H17v-2q0-2.075-1.463-3.537T12 6q-.675 0-1.3.163t-1.2.512l-1.45-1.45q.875-.6 1.863-.912T12 4q2.925 0 4.963 2.038T19 11q1.725.2 2.863 1.488T23 15.5q0 .975-.375 1.813T21.6 18.75m-6.775-6.725"
+		></path>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "CloudOffline",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/Sync.vue b/assets/js/components/MaterialIcon/Sync.vue
new file mode 100644
index 00000000..7da9f2d1
--- /dev/null
+++ b/assets/js/components/MaterialIcon/Sync.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<path
+			fill="currentColor"
+			d="M6 12.05q0 1.125.425 2.188T7.75 16.2l.25.25V15q0-.425.288-.712T9 14q.425 0 .713.288T10 15v4q0 .425-.288.713T9 20H5q-.425 0-.712-.288T4 19q0-.425.288-.712T5 18h1.75l-.4-.35q-1.3-1.15-1.825-2.625T4 12.05Q4 9.7 5.2 7.787T8.425 4.85q.35-.2.738-.025t.512.575q.125.375-.012.75t-.488.575q-1.45.8-2.312 2.213T6 12.05m12-.1q0-1.125-.425-2.187T16.25 7.8L16 7.55V9q0 .425-.288.713T15 10q-.425 0-.712-.288T14 9V5q0-.425.288-.712T15 4h4q.425 0 .713.288T20 5q0 .425-.288.713T19 6h-1.75l.4.35q1.225 1.225 1.788 2.663T20 11.95q0 2.35-1.2 4.263t-3.225 2.937q-.35.2-.737.025t-.513-.575q-.125-.375.013-.75t.487-.575q1.45-.8 2.313-2.212T18 11.95"
+		></path>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "Sync",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/Site.vue b/assets/js/components/Site.vue
index 27458068..7104b2ca 100644
--- a/assets/js/components/Site.vue
+++ b/assets/js/components/Site.vue
@@ -1,7 +1,7 @@
 <template>
 	<div class="d-flex flex-column site safe-area-inset">
 		<div class="container px-4 top-area">
-			<div class="d-flex justify-content-between align-items-center mb-2">
+			<div class="d-flex justify-content-between align-items-center my-3 my-md-4">
 				<h1 class="d-block my-0">
 					{{ siteTitle || "evcc" }}
 				</h1>
@@ -63,10 +63,8 @@ export default {
 		gridConfigured: Boolean,
 		gridPower: Number,
 		homePower: Number,
-		pvConfigured: Boolean,
 		pvPower: Number,
 		pv: Array,
-		batteryConfigured: Boolean,
 		batteryPower: Number,
 		batterySoc: Number,
 		batteryDischargeControl: Boolean,
@@ -104,6 +102,12 @@ export default {
 		smartCostActive: Boolean,
 	},
 	computed: {
+		batteryConfigured: function () {
+			return this.battery?.length;
+		},
+		pvConfigured: function () {
+			return this.pv?.length;
+		},
 		energyflow: function () {
 			return this.collectProps(Energyflow);
 		},
diff --git a/assets/js/components/TopHeader.vue b/assets/js/components/TopHeader.vue
index 56d6e91b..e3d70ce0 100644
--- a/assets/js/components/TopHeader.vue
+++ b/assets/js/components/TopHeader.vue
@@ -1,5 +1,5 @@
 <template>
-	<header class="d-flex justify-content-between align-items-center py-3">
+	<header class="d-flex justify-content-between align-items-center py-3 py-md-4">
 		<h1 class="mb-1 pt-1 d-flex text-nowrap text-truncate">
 			<router-link class="evcc-default-text" to="/" data-testid="home-link">
 				<shopicon-regular-home size="s" class="home"></shopicon-regular-home>
diff --git a/assets/js/components/TopNavigation.vue b/assets/js/components/TopNavigation.vue
index 7da18c2b..102a0d40 100644
--- a/assets/js/components/TopNavigation.vue
+++ b/assets/js/components/TopNavigation.vue
@@ -124,7 +124,7 @@ export default {
 		},
 		sponsor: String,
 		sponsorTokenExpires: Number,
-		batteryConfigured: Boolean,
+		battery: Array,
 	},
 	data() {
 		return {
@@ -132,6 +132,9 @@ export default {
 		};
 	},
 	computed: {
+		batteryConfigured: function () {
+			return this.battery?.length;
+		},
 		logoutCount() {
 			return this.providerLogins.filter((login) => !login.loggedIn).length;
 		},
diff --git a/assets/js/components/Vehicle.vue b/assets/js/components/Vehicle.vue
index 7a2f3001..31159efe 100644
--- a/assets/js/components/Vehicle.vue
+++ b/assets/js/components/Vehicle.vue
@@ -123,6 +123,7 @@ export default {
 		vehicles: Array,
 		vehicleSoc: Number,
 		vehicleTargetSoc: Number,
+		vehicleNotReachable: Boolean,
 	},
 	emits: ["limit-soc-updated", "limit-energy-updated", "change-vehicle", "remove-vehicle"],
 	data() {
diff --git a/assets/js/components/VehicleTitle.vue b/assets/js/components/VehicleTitle.vue
index e413ede3..e31a0990 100644
--- a/assets/js/components/VehicleTitle.vue
+++ b/assets/js/components/VehicleTitle.vue
@@ -1,13 +1,15 @@
 <template>
 	<div class="d-flex justify-content-between mb-3 align-items-center" data-testid="vehicle-title">
 		<h4 class="d-flex align-items-center m-0 flex-grow-1 overflow-hidden">
-			<shopicon-regular-refresh
+			<div
 				v-if="iconType === 'refresh'"
+				class="me-2 flex-shrink-0 spin"
 				ref="refresh"
-				data-bs-toggle="tooltip"
 				:title="$t('main.vehicle.detectionActive')"
-				class="me-2 flex-shrink-0 spin"
-			></shopicon-regular-refresh>
+				data-bs-toggle="tooltip"
+			>
+				<Sync />
+			</div>
 			<VehicleIcon
 				v-else-if="iconType === 'vehicle'"
 				:name="icon"
@@ -26,38 +28,59 @@
 				@change-vehicle="changeVehicle"
 				@remove-vehicle="removeVehicle"
 			>
-				<span class="flex-grow-1 text-truncate vehicle-name">
+				<span class="flex-grow-1 text-truncate vehicle-name" data-testid="vehicle-name">
 					{{ name }}
 				</span>
 			</VehicleOptions>
-			<span v-else class="flex-grow-1 text-truncate vehicle-name">
+			<span v-else class="flex-grow-1 text-truncate vehicle-name" data-testid="vehicle-name">
 				{{ name }}
 			</span>
+			<button
+				v-if="vehicleNotReachable"
+				class="ms-2 btn-neutral"
+				ref="notReachable"
+				data-bs-toggle="tooltip"
+				:title="$t('main.vehicle.notReachable')"
+				type="button"
+				data-testid="vehicle-not-reachable-icon"
+				@click="openHelpModal"
+			>
+				<CloudOffline class="evcc-gray" />
+			</button>
 		</h4>
 	</div>
 </template>
 
 <script>
-import "@h2d2/shopicons/es/regular/refresh";
 import "@h2d2/shopicons/es/regular/cablecharge";
 import Tooltip from "bootstrap/js/dist/tooltip";
+import Modal from "bootstrap/js/dist/modal";
 import VehicleIcon from "./VehicleIcon";
 import VehicleOptions from "./VehicleOptions.vue";
+import CloudOffline from "./MaterialIcon/CloudOffline.vue";
+import Sync from "./MaterialIcon/Sync.vue";
 import collector from "../mixins/collector";
 
 export default {
 	name: "VehicleTitle",
-	components: { VehicleOptions, VehicleIcon },
+	components: { VehicleOptions, VehicleIcon, Sync, CloudOffline },
 	mixins: [collector],
 	props: {
 		connected: Boolean,
 		id: [String, Number],
 		vehicleDetectionActive: Boolean,
+		vehicleNotReachable: Boolean,
 		icon: String,
 		vehicleName: String,
 		vehicles: { type: Array, default: () => [] },
 		title: String,
 	},
+	data: function () {
+		return {
+			refreshTooltip: null,
+			notReachableTooltip: null,
+		};
+	},
 	emits: ["change-vehicle", "remove-vehicle"],
 	computed: {
 		iconType() {
@@ -93,11 +116,11 @@ export default {
 	},
 	watch: {
 		iconType: function () {
-			this.tooltip();
+			this.initTooltip();
 		},
 	},
 	mounted: function () {
-		this.tooltip();
+		this.initTooltip();
 	},
 	methods: {
 		changeVehicle(name) {
@@ -106,21 +129,28 @@ export default {
 		removeVehicle() {
 			this.$emit("remove-vehicle");
 		},
-		tooltip() {
+		initTooltip() {
 			this.$nextTick(() => {
+				this.refreshTooltip?.dispose();
+				this.notReachableTooltip?.dispose();
 				if (this.$refs.refresh) {
-					new Tooltip(this.$refs.refresh);
+					this.refreshTooltip = new Tooltip(this.$refs.refresh);
+				}
+				if (this.$refs.notReachable) {
+					this.notReachableTooltip = new Tooltip(this.$refs.notReachable);
 				}
 			});
 		},
+		openHelpModal() {
+			const modal = Modal.getOrCreateInstance(document.getElementById("helpModal"));
+			modal.show();
+			this.initTooltip();
+		},
 	},
 };
 </script>
 
 <style scoped>
-.options {
-	margin-right: -0.75rem;
-}
 .vehicle-name {
 	text-decoration-color: var(--evcc-gray);
 }
@@ -130,16 +160,12 @@ export default {
 .spin {
 	animation: rotation 1s infinite cubic-bezier(0.37, 0, 0.63, 1);
 }
-.spin :deep(svg) {
-	/* workaround to fix the not perfectly centered shopicon. Remove once its fixed in @h2d2/shopicons */
-	transform: translateY(-0.7px);
-}
 @keyframes rotation {
 	from {
 		transform: rotate(0deg);
 	}
 	to {
-		transform: rotate(360deg);
+		transform: rotate(-360deg);
 	}
 }
 </style>
diff --git a/assets/js/mixins/icon.js b/assets/js/mixins/icon.js
new file mode 100644
index 00000000..8b997cab
--- /dev/null
+++ b/assets/js/mixins/icon.js
@@ -0,0 +1,22 @@
+export default {
+  props: {
+    size: {
+      type: String,
+      validator: function (value) {
+        return ["s", "m", "l", "xl"].includes(value);
+      },
+    },
+  },
+  computed: {
+    svgStyle() {
+      const sizes = {
+        s: "24px",
+        m: "32px",
+        l: "48px",
+        xl: "64px",
+      };
+      const size = sizes[this.size] || sizes.s;
+      return { display: "block", width: size, height: size };
+    },
+  },
+};
diff --git a/assets/js/views/App.vue b/assets/js/views/App.vue
index 43c5e003..9a01e69a 100644
--- a/assets/js/views/App.vue
+++ b/assets/js/views/App.vue
@@ -40,9 +40,20 @@ export default {
 		const siteTitle = store.state.siteTitle;
 		return { title: siteTitle ? `${siteTitle} | evcc` : "evcc" };
 	},
+	watch: {
+		version: function (prev, now) {
+			if (!!prev && !!now) {
+				console.log("new version detected. reloading browser", { now, prev });
+				this.reload();
+			}
+		},
+	},
 	computed: {
+		version: function () {
+			return store.state.version;
+		},
 		batteryModalAvailabe: function () {
-			return store.state.batteryConfigured;
+			return store.state.battery?.length;
 		},
 		globalSettingsProps: function () {
 			return this.collectProps(GlobalSettingsModal, store.state);
diff --git a/charger/mennekes-compact.go b/charger/mennekes-compact.go
index aff880b7..b713d0cc 100644
--- a/charger/mennekes-compact.go
+++ b/charger/mennekes-compact.go
@@ -116,7 +116,8 @@ func NewMennekesCompact(uri, device, comset string, baudrate int, proto modbus.P
 }
 
 func (wb *MennekesCompact) heartbeat(timeout time.Duration) {
-	for range time.Tick(timeout) {
+	tick := time.NewTicker(timeout)
+	for ; true; <-tick.C {
 		if _, err := wb.conn.WriteSingleRegister(mennekesRegHeartbeat, mennekesHeartbeatToken); err != nil {
 			wb.log.ERROR.Println("heartbeat:", err)
 		}
diff --git a/charger/menneckes-hcc3.go b/charger/mennekes-hcc3.go
similarity index 60%
rename from charger/menneckes-hcc3.go
rename to charger/mennekes-hcc3.go
index bdda1b54..8b762540 100644
--- a/charger/menneckes-hcc3.go
+++ b/charger/mennekes-hcc3.go
@@ -32,28 +32,28 @@ import (
 
 // https://update.mennekes.de/hcc3/1.13/Description%20Modbus_AMTRON%20HCC3_v01_2021-06-25_en.pdf
 
-// MenneckesHcc3 Xtra/Premium charger implementation
-type MenneckesHcc3 struct {
+// MennekesHcc3 Xtra/Premium charger implementation
+type MennekesHcc3 struct {
 	conn *modbus.Connection
 	curr uint16
 }
 
 const (
-	menneckesHcc3RegStatus     = 0x0302
-	menneckesHcc3RegPhases     = 0x0308
-	menneckesHcc3RegSerial     = 0x030B
-	menneckesHcc3RegEnergy     = 0x030D
-	menneckesHcc3RegName       = 0x0311
-	menneckesHcc3RegPower      = 0x030F
-	menneckesHcc3RegAmpsConfig = 0x0400
+	mennekesHcc3RegStatus     = 0x0302
+	mennekesHcc3RegPhases     = 0x0308
+	mennekesHcc3RegSerial     = 0x030B
+	mennekesHcc3RegEnergy     = 0x030D
+	mennekesHcc3RegName       = 0x0311
+	mennekesHcc3RegPower      = 0x030F
+	mennekesHcc3RegAmpsConfig = 0x0400
 )
 
 func init() {
-	registry.Add("menneckes-hcc3", NewMenneckesHcc3FromConfig)
+	registry.Add("mennekes-hcc3", NewMennekesHcc3FromConfig)
 }
 
-// NewMenneckesHcc3FromConfig creates a Mennekes menneckesHcc3 charger from generic config
-func NewMenneckesHcc3FromConfig(other map[string]interface{}) (api.Charger, error) {
+// NewMennekesHcc3FromConfig creates a Mennekes mennekesHcc3 charger from generic config
+func NewMennekesHcc3FromConfig(other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 255,
 	}
@@ -62,11 +62,11 @@ func NewMenneckesHcc3FromConfig(other map[string]interface{}) (api.Charger, erro
 		return nil, err
 	}
 
-	return NewMenneckesHcc3(cc.URI, cc.ID)
+	return NewMennekesHcc3(cc.URI, cc.ID)
 }
 
-// NewMenneckesHcc3 creates Menneckes HCC3 charger
-func NewMenneckesHcc3(uri string, slaveID uint8) (api.Charger, error) {
+// NewMennekesHcc3 creates Mennekes HCC3 charger
+func NewMennekesHcc3(uri string, slaveID uint8) (api.Charger, error) {
 	uri = util.DefaultPort(uri, 502)
 
 	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
@@ -78,10 +78,10 @@ func NewMenneckesHcc3(uri string, slaveID uint8) (api.Charger, error) {
 		return nil, api.ErrSponsorRequired
 	}
 
-	log := util.NewLogger("menneckes-hcc3")
+	log := util.NewLogger("mennekes-hcc3")
 	conn.Logger(log.TRACE)
 
-	wb := &MenneckesHcc3{
+	wb := &MennekesHcc3{
 		conn: conn,
 		curr: 6,
 	}
@@ -90,8 +90,8 @@ func NewMenneckesHcc3(uri string, slaveID uint8) (api.Charger, error) {
 }
 
 // Status implements the api.Charger interface
-func (wb *MenneckesHcc3) Status() (api.ChargeStatus, error) {
-	b, err := wb.conn.ReadInputRegisters(menneckesHcc3RegStatus, 1)
+func (wb *MennekesHcc3) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadInputRegisters(mennekesHcc3RegStatus, 1)
 	if err != nil {
 		return api.StatusNone, err
 	}
@@ -111,8 +111,8 @@ func (wb *MenneckesHcc3) Status() (api.ChargeStatus, error) {
 }
 
 // Enabled implements the api.Charger interface
-func (wb *MenneckesHcc3) Enabled() (bool, error) {
-	b, err := wb.conn.ReadHoldingRegisters(menneckesHcc3RegAmpsConfig, 1)
+func (wb *MennekesHcc3) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(mennekesHcc3RegAmpsConfig, 1)
 	if err != nil {
 		return false, err
 	}
@@ -123,25 +123,25 @@ func (wb *MenneckesHcc3) Enabled() (bool, error) {
 }
 
 // Enable implements the api.Charger interface
-func (wb *MenneckesHcc3) Enable(enable bool) error {
+func (wb *MennekesHcc3) Enable(enable bool) error {
 	var u uint16
 	if enable {
 		u = wb.curr
 	}
 
-	_, err := wb.conn.WriteSingleRegister(menneckesHcc3RegAmpsConfig, u)
+	_, err := wb.conn.WriteSingleRegister(mennekesHcc3RegAmpsConfig, u)
 	return err
 }
 
 // MaxCurrent implements the api.Charger interface
-func (wb *MenneckesHcc3) MaxCurrent(current int64) error {
+func (wb *MennekesHcc3) MaxCurrent(current int64) error {
 	if current < 6 {
 		return fmt.Errorf("invalid current %d", current)
 	}
 
 	cur := uint16(current)
 
-	_, err := wb.conn.WriteSingleRegister(menneckesHcc3RegAmpsConfig, cur)
+	_, err := wb.conn.WriteSingleRegister(mennekesHcc3RegAmpsConfig, cur)
 	if err == nil {
 		wb.curr = cur
 	}
@@ -149,11 +149,11 @@ func (wb *MenneckesHcc3) MaxCurrent(current int64) error {
 	return err
 }
 
-var _ api.Meter = (*MenneckesHcc3)(nil)
+var _ api.Meter = (*MennekesHcc3)(nil)
 
 // CurrentPower implements the api.Meter interface
-func (wb *MenneckesHcc3) CurrentPower() (float64, error) {
-	b, err := wb.conn.ReadInputRegisters(menneckesHcc3RegPower, 2)
+func (wb *MennekesHcc3) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(mennekesHcc3RegPower, 2)
 	if err != nil {
 		return 0, err
 	}
@@ -161,11 +161,11 @@ func (wb *MenneckesHcc3) CurrentPower() (float64, error) {
 	return rs485.RTUUint32ToFloat64Swapped(b), nil
 }
 
-var _ api.ChargeRater = (*MenneckesHcc3)(nil)
+var _ api.ChargeRater = (*MennekesHcc3)(nil)
 
 // ChargedEnergy implements the api.MeterEnergy interface
-func (wb *MenneckesHcc3) ChargedEnergy() (float64, error) {
-	b, err := wb.conn.ReadInputRegisters(menneckesHcc3RegEnergy, 2)
+func (wb *MennekesHcc3) ChargedEnergy() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(mennekesHcc3RegEnergy, 2)
 	if err != nil {
 		return 0, err
 	}
@@ -173,19 +173,19 @@ func (wb *MenneckesHcc3) ChargedEnergy() (float64, error) {
 	return rs485.RTUUint32ToFloat64Swapped(b) / 1e3, nil
 }
 
-var _ api.Diagnosis = (*MenneckesHcc3)(nil)
+var _ api.Diagnosis = (*MennekesHcc3)(nil)
 
 // Diagnose implements the api.Diagnosis interface
-func (wb *MenneckesHcc3) Diagnose() {
-	if b, err := wb.conn.ReadInputRegisters(menneckesHcc3RegName, 11); err == nil {
+func (wb *MennekesHcc3) Diagnose() {
+	if b, err := wb.conn.ReadInputRegisters(mennekesHcc3RegName, 11); err == nil {
 		fmt.Printf("Name: %s\n", encoding.StringLsbFirst(b))
 	}
 
-	if b, err := wb.conn.ReadInputRegisters(menneckesHcc3RegPhases, 1); err == nil {
+	if b, err := wb.conn.ReadInputRegisters(mennekesHcc3RegPhases, 1); err == nil {
 		fmt.Printf("Phases: %d\n", binary.BigEndian.Uint16(b))
 	}
 
-	if b, err := wb.conn.ReadInputRegisters(menneckesHcc3RegSerial, 2); err == nil {
+	if b, err := wb.conn.ReadInputRegisters(mennekesHcc3RegSerial, 2); err == nil {
 		fmt.Printf("Serial: %d\n", binary.LittleEndian.Uint32(b))
 	}
 }
diff --git a/charger/ocpp.go b/charger/ocpp.go
index 9404c370..b98c6758 100644
--- a/charger/ocpp.go
+++ b/charger/ocpp.go
@@ -174,6 +174,9 @@ func NewOCPP(id string, connector int, idtag string,
 			meterInterval = 10 * time.Second
 		}
 	} else {
+		// fix timing issue in EVBox when switching OCPP protocol version
+		time.Sleep(time.Second)
+
 		err := ocpp.Instance().GetConfiguration(cp.ID(), func(resp *core.GetConfigurationConfirmation, err error) {
 			if err == nil {
 				// log unsupported configuration keys
@@ -335,8 +338,8 @@ func (c *OCPP) Enable(enable bool) (err error) {
 
 	if enable {
 		if txn > 0 {
-			// we have the transaction id, switch from 0A to last current
-			return c.updatePeriod(c.current)
+			// we have the transaction id, treat as enabled
+			return nil
 		}
 
 		err = ocpp.Instance().RemoteStartTransaction(c.conn.ChargePoint().ID(), func(resp *core.RemoteStartTransactionConfirmation, err error) {
@@ -350,25 +353,31 @@ func (c *OCPP) Enable(enable bool) (err error) {
 			request.ConnectorId = &connector
 			request.ChargingProfile = c.getTxChargingProfile(c.current, 0)
 		})
+	} else {
+		// if no transaction is running, the vehicle may have stopped it (which is ok) or an unknown transaction is running
+		if txn == 0 {
+			// we cannot tell if a transaction is really running, so we check the status
+			status, err := c.Status()
+			if err != nil {
+				return err
+			}
+			if status == api.StatusC {
+				return errors.New("cannot disable: unknown transaction running")
+			}
 
-		return c.wait(err, rc)
-	}
-
-	// if no transaction is running, the vehicle may have stopped it (which is ok) or an unknown transaction is running
-	if txn == 0 {
-		// we cannot tell if a transaction is really running, so we check the status
-		status, err := c.Status()
-		if err != nil {
-			return err
-		}
-		if status == api.StatusC {
-			return errors.New("cannot disable: unknown transaction running")
+			return nil
 		}
 
-		return nil
+		err = ocpp.Instance().RemoteStopTransaction(c.conn.ChargePoint().ID(), func(resp *core.RemoteStopTransactionConfirmation, err error) {
+			if err == nil && resp != nil && resp.Status != types.RemoteStartStopStatusAccepted {
+				err = errors.New(string(resp.Status))
+			}
+
+			rc <- err
+		}, txn)
 	}
 
-	return c.updatePeriod(0)
+	return c.wait(err, rc)
 }
 
 func (c *OCPP) setChargingProfile(profile *types.ChargingProfile) error {
diff --git a/charger/ocpp/connector.go b/charger/ocpp/connector.go
index 3dc4e6aa..9ffbd194 100644
--- a/charger/ocpp/connector.go
+++ b/charger/ocpp/connector.go
@@ -73,7 +73,8 @@ func (conn *Connector) TriggerMessageRequest(feature remotetrigger.MessageTrigge
 // WatchDog triggers meter values messages if older than timeout.
 // Must be wrapped in a goroutine.
 func (conn *Connector) WatchDog(timeout time.Duration) {
-	for ; true; <-time.Tick(timeout) {
+	tick := time.NewTicker(timeout)
+	for ; true; <-tick.C {
 		conn.mu.Lock()
 		update := conn.txnId != 0 && conn.clock.Since(conn.meterUpdated) > timeout
 		conn.mu.Unlock()
diff --git a/charger/openwb-pro.go b/charger/openwb-pro.go
index a8922d72..87298598 100644
--- a/charger/openwb-pro.go
+++ b/charger/openwb-pro.go
@@ -215,5 +215,9 @@ func (wb *OpenWBPro) Identify() (string, error) {
 		return "", err
 	}
 
-	return res.VehicleID, err
+	if res.VehicleID != "" {
+		return res.VehicleID, nil
+	}
+
+	return res.RfidTag, nil
 }
diff --git a/charger/openwb/pro/types.go b/charger/openwb/pro/types.go
index b251baf9..2f5d319e 100644
--- a/charger/openwb/pro/types.go
+++ b/charger/openwb/pro/types.go
@@ -18,6 +18,7 @@ type Status struct {
 	PhasesInUse    int     `json:"phases_in_use"`
 	OfferedCurrent float64 `json:"offered_current"`
 	EvseSignaling  string  `json:"evse_signaling"`
+	RfidTag        string  `json:"rfid_tag"`
 	VehicleID      string  `json:"vehicle_id"`
 	Soc            int     `json:"soc_value"`
 	SocTimestamp   int64   `json:"soc_timestamp"`
diff --git a/charger/twc3.go b/charger/twc3.go
index e383950a..c590637d 100644
--- a/charger/twc3.go
+++ b/charger/twc3.go
@@ -16,10 +16,7 @@ import (
 
 // Twc3 is an api.Vehicle implementation for Twc3 cars
 type Twc3 struct {
-	*request.Helper
-	log     *util.Logger
 	lp      loadpoint.API
-	uri     string
 	vitalsG func() (Vitals, error)
 	enabled bool
 }
@@ -71,20 +68,16 @@ func NewTwc3FromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	log := util.NewLogger("twc3")
+	c := &Twc3{}
 
-	c := &Twc3{
-		Helper: request.NewHelper(log),
-		uri:    util.DefaultScheme(strings.TrimSuffix(cc.URI, "/"), "http"),
-		log:    log,
-	}
+	client := request.NewHelper(util.NewLogger("twc3"))
+	uri := fmt.Sprintf("%s/api/1/vitals", util.DefaultScheme(strings.TrimSuffix(cc.URI, "/"), "http"))
 
 	c.vitalsG = provider.Cached(func() (Vitals, error) {
 		var res Vitals
-		uri := fmt.Sprintf("%s/api/1/vitals", c.uri)
-		err := c.GetJSON(uri, &res)
+		err := client.GetJSON(uri, &res)
 		return res, err
-	}, time.Second)
+	}, cc.Cache)
 
 	return c, nil
 }
diff --git a/cmd/check_config.go b/cmd/check_config.go
index ad0cc23e..da265bb2 100644
--- a/cmd/check_config.go
+++ b/cmd/check_config.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	_ "embed"
+	"fmt"
 	"os"
 
 	"github.com/spf13/cobra"
@@ -24,6 +25,6 @@ func runConfigCheck(cmd *cobra.Command, args []string) {
 		log.FATAL.Println("config invalid:", err)
 		os.Exit(1)
 	} else {
-		log.INFO.Println("config valid")
+		fmt.Println("config valid")
 	}
 }
diff --git a/cmd/root.go b/cmd/root.go
index 3a002405..c7f7349a 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -264,6 +264,9 @@ func runRoot(cmd *cobra.Command, args []string) {
 			go updater.Run(log, httpd, valueChan)
 		}
 
+		// remove previous fatal startup errors
+		valueChan <- util.Param{Key: "fatal", Val: nil}
+
 		// expose sponsor to UI
 		if sponsor.Subject != "" {
 			valueChan <- util.Param{Key: "sponsor", Val: sponsor.Subject}
diff --git a/cmd/setup.go b/cmd/setup.go
index 02ee0128..14470200 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -153,6 +153,13 @@ func (c networkConfig) URI() string {
 	return fmt.Sprintf("%s://%s", c.Schema, c.HostPort())
 }
 
+func nameValid(name string) error {
+	if !nameRE.MatchString(name) {
+		return fmt.Errorf("name must not contain special characters or spaces: %s", name)
+	}
+	return nil
+}
+
 func loadConfigFile(conf *globalConfig) error {
 	err := viper.ReadInConfig()
 
@@ -186,6 +193,10 @@ func configureMeters(static []config.Named, names ...string) error {
 			continue
 		}
 
+		if err := nameValid(cc.Name); err != nil {
+			log.WARN.Printf("create meter %d: %v", i+1, err)
+		}
+
 		instance, err := meter.NewFromConfig(cc.Type, cc.Other)
 		if err != nil {
 			return fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)
@@ -234,6 +245,10 @@ func configureChargers(static []config.Named, names ...string) error {
 			continue
 		}
 
+		if err := nameValid(cc.Name); err != nil {
+			log.WARN.Printf("create charger %d: %v", i+1, err)
+		}
+
 		g.Go(func() error {
 			instance, err := charger.NewFromConfig(cc.Type, cc.Other)
 			if err != nil {
@@ -271,10 +286,6 @@ func configureChargers(static []config.Named, names ...string) error {
 }
 
 func vehicleInstance(cc config.Named) (api.Vehicle, error) {
-	if !nameRE.MatchString(cc.Name) {
-		return nil, fmt.Errorf("vehicle name must not contain special characters or spaces: %s", cc.Name)
-	}
-
 	instance, err := vehicle.NewFromConfig(cc.Type, cc.Other)
 	if err != nil {
 		var ce *util.ConfigError
@@ -284,7 +295,7 @@ func vehicleInstance(cc config.Named) (api.Vehicle, error) {
 
 		// wrap non-config vehicle errors to prevent fatals
 		log.ERROR.Printf("creating vehicle %s failed: %v", cc.Name, err)
-		instance = vehicle.NewWrapper(cc.Name, cc.Other, err)
+		instance = vehicle.NewWrapper(cc.Name, cc.Type, cc.Other, err)
 	}
 
 	// ensure vehicle config has title
@@ -312,6 +323,10 @@ func configureVehicles(static []config.Named, names ...string) error {
 			continue
 		}
 
+		if err := nameValid(cc.Name); err != nil {
+			return fmt.Errorf("cannot create vehicle %d: %w", i+1, err)
+		}
+
 		g.Go(func() error {
 			instance, err := vehicleInstance(cc)
 			if err != nil {
diff --git a/core/keys/site.go b/core/keys/site.go
index e930f7ba..8138e2df 100644
--- a/core/keys/site.go
+++ b/core/keys/site.go
@@ -14,7 +14,6 @@ const (
 	HomePower             = "homePower"
 	PrioritySoc           = "prioritySoc"
 	Pv                    = "pv"
-	PvConfigured          = "pvConfigured"
 	PvEnergy              = "pvEnergy"
 	PvPower               = "pvPower"
 	ResidualPower         = "residualPower"
@@ -43,10 +42,9 @@ const (
 	BufferStartSoc          = "bufferStartSoc"
 
 	// battery status
-	Battery           = "battery"
-	BatteryConfigured = "batteryConfigured"
-	BatteryEnergy     = "batteryEnergy"
-	BatteryMode       = "batteryMode"
-	BatteryPower      = "batteryPower"
-	BatterySoc        = "batterySoc"
+	Battery       = "battery"
+	BatteryEnergy = "batteryEnergy"
+	BatteryMode   = "batteryMode"
+	BatteryPower  = "batteryPower"
+	BatterySoc    = "batterySoc"
 )
diff --git a/core/loadpoint.go b/core/loadpoint.go
index cb8df4d1..bec45974 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -237,7 +237,7 @@ func NewLoadpointFromConfig(log *util.Logger, settings *Settings, other map[stri
 
 	// TODO deprecated
 	if lp.MinCurrent_ > 0 {
-		lp.log.WARN.Println("deprecated: minCurrent setting is ignored, please remove")
+		lp.log.WARN.Println("deprecated: mincurrent setting is ignored, please remove")
 		if _, err := lp.settings.Float(keys.MinCurrent); err != nil {
 			lp.settings.SetFloat(keys.MinCurrent, lp.MinCurrent_)
 		}
@@ -658,6 +658,11 @@ func (lp *Loadpoint) syncCharger() error {
 		return err
 	}
 
+	// some chargers (i.E. Easee in some configurations) disable themself to be able to switch phases
+	if !enabled && lp.enabled && !lp.phaseSwitchCompleted() {
+		return lp.charger.Enable(true) // enable charger
+	}
+
 	if lp.chargerUpdateCompleted() {
 		defer func() {
 			lp.enabled = enabled
@@ -1422,7 +1427,7 @@ func (lp *Loadpoint) publishSocAndRange() {
 				targetSoc = int(math.Trunc(limit))
 				lp.log.DEBUG.Printf("vehicle soc limit: %.0f%%", limit)
 				lp.publish(keys.VehicleTargetSoc, limit)
-			} else {
+			} else if !errors.Is(err, api.ErrNotAvailable) {
 				lp.log.ERROR.Printf("vehicle soc limit: %v", err)
 			}
 		}
diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go
index 57a441f6..73ee4468 100644
--- a/core/loadpoint_effective.go
+++ b/core/loadpoint_effective.go
@@ -60,26 +60,29 @@ func (lp *Loadpoint) SocBasedPlanning() bool {
 
 // effectiveMinCurrent returns the effective min current
 func (lp *Loadpoint) effectiveMinCurrent() float64 {
-	minCurrent := lp.GetMinCurrent()
+	lpMin := lp.GetMinCurrent()
+	var vehicleMin, chargerMin float64
 
 	if v := lp.GetVehicle(); v != nil {
 		if res, ok := v.OnIdentified().GetMinCurrent(); ok {
-			minCurrent = max(minCurrent, res)
+			vehicleMin = res
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
 		if res, _, err := c.GetMinMaxCurrent(); err == nil {
-			if res > 0 && res < minCurrent {
-				minCurrent = res
-			} else {
-				minCurrent = max(minCurrent, res)
-			}
-			lp.publish(keys.EffectiveMinCurrent, minCurrent)
+			chargerMin = res
 		}
 	}
 
-	return minCurrent
+	switch {
+	case max(vehicleMin, chargerMin) == 0:
+		return lpMin
+	case chargerMin > 0:
+		return max(vehicleMin, chargerMin)
+	default:
+		return max(vehicleMin, lpMin)
+	}
 }
 
 // effectiveMaxCurrent returns the effective max current
@@ -87,15 +90,14 @@ func (lp *Loadpoint) effectiveMaxCurrent() float64 {
 	maxCurrent := lp.GetMaxCurrent()
 
 	if v := lp.GetVehicle(); v != nil {
-		if res, ok := v.OnIdentified().GetMaxCurrent(); ok {
+		if res, ok := v.OnIdentified().GetMaxCurrent(); ok && res > 0 {
 			maxCurrent = min(maxCurrent, res)
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
-		if _, res, err := c.GetMinMaxCurrent(); err == nil {
+		if _, res, err := c.GetMinMaxCurrent(); err == nil && res > 0 {
 			maxCurrent = min(maxCurrent, res)
-			lp.publish(keys.EffectiveMaxCurrent, maxCurrent)
 		}
 	}
 
diff --git a/core/site.go b/core/site.go
index 8e6c9a44..cd43ca55 100644
--- a/core/site.go
+++ b/core/site.go
@@ -814,8 +814,8 @@ func (site *Site) prepare() {
 	site.publish(keys.SiteTitle, site.Title)
 
 	site.publish(keys.GridConfigured, site.gridMeter != nil)
-	site.publish(keys.PvConfigured, len(site.pvMeters) > 0)
-	site.publish(keys.BatteryConfigured, len(site.batteryMeters) > 0)
+	site.publish(keys.Pv, make([]api.Meter, len(site.pvMeters)))
+	site.publish(keys.Battery, make([]api.Meter, len(site.batteryMeters)))
 	site.publish(keys.BufferSoc, site.bufferSoc)
 	site.publish(keys.BufferStartSoc, site.bufferStartSoc)
 	site.publish(keys.PrioritySoc, site.prioritySoc)
diff --git a/go.mod b/go.mod
index b15477ea..5c4cf6ee 100644
--- a/go.mod
+++ b/go.mod
@@ -7,12 +7,12 @@ require (
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/BurntSushi/toml v1.3.2
 	github.com/Masterminds/sprig/v3 v3.2.3
-	github.com/PuerkitoBio/goquery v1.9.0
+	github.com/PuerkitoBio/goquery v1.9.1
 	github.com/andig/go-powerwall v0.2.1-0.20230808194509-dd70cdb6e140
 	github.com/andig/gosunspec v0.0.0-20231205122018-1daccfa17912
 	github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e
 	github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
-	github.com/aws/aws-sdk-go v1.50.24
+	github.com/aws/aws-sdk-go v1.50.38
 	github.com/basgys/goxml2json v1.1.0
 	github.com/basvdlei/gotsmart v0.0.3
 	github.com/benbjohnson/clock v1.3.5
@@ -29,13 +29,13 @@ require (
 	github.com/enbility/eebus-go v0.2.0
 	github.com/evcc-io/tesla-proxy-client v0.0.0-20240221194046-4168b3759701
 	github.com/fatih/structs v1.1.0
-	github.com/glebarez/sqlite v1.10.0
+	github.com/glebarez/sqlite v1.11.0
 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
-	github.com/go-playground/validator/v10 v10.18.0
+	github.com/go-playground/validator/v10 v10.19.0
 	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-20240113102150-4ac511a17e33
-	github.com/golang-jwt/jwt/v5 v5.2.0
+	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/google/go-github/v32 v32.1.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
@@ -44,7 +44,7 @@ require (
 	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
 	github.com/grid-x/modbus v0.0.0-20240214112450-0d4922fba364
 	github.com/hashicorp/go-version v1.6.0
-	github.com/hasura/go-graphql-client v0.11.0
+	github.com/hasura/go-graphql-client v0.12.1
 	github.com/influxdata/influxdb-client-go/v2 v2.13.0
 	github.com/insomniacslk/tapo v1.0.0
 	github.com/itchyny/gojq v0.12.14
@@ -58,7 +58,7 @@ require (
 	github.com/koron/go-ssdp v0.0.4
 	github.com/korylprince/ipnetgen v1.0.1
 	github.com/libp2p/zeroconf/v2 v2.2.0
-	github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc
+	github.com/lorenzodonini/ocpp-go v0.18.0
 	github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
 	github.com/mabunixda/wattpilot v1.6.6
 	github.com/mitchellh/go-homedir v1.1.0
@@ -70,9 +70,9 @@ require (
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/philippseith/signalr v0.6.3
 	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
-	github.com/prometheus-community/pro-bing v0.3.0
-	github.com/prometheus/client_golang v1.18.0
-	github.com/prometheus/common v0.48.0
+	github.com/prometheus-community/pro-bing v0.4.0
+	github.com/prometheus/client_golang v1.19.0
+	github.com/prometheus/common v0.50.0
 	github.com/robertkrimen/otto v0.3.0
 	github.com/samber/lo v1.39.0
 	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
@@ -84,20 +84,20 @@ require (
 	github.com/spf13/viper v1.18.2
 	github.com/stretchr/testify v1.8.4
 	github.com/teslamotors/vehicle-command v0.0.2
-	github.com/traefik/yaegi v0.15.1
+	github.com/traefik/yaegi v0.16.0
 	github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
-	github.com/volkszaehler/mbmd v0.0.0-20231215091549-af16b1f597b9
+	github.com/volkszaehler/mbmd v0.0.0-20240229124119-740c5c8ad344
 	github.com/writeas/go-strip-markdown/v2 v2.1.1
 	gitlab.com/bboehmke/sunny v0.16.0
 	go.uber.org/mock v0.4.0
-	golang.org/x/crypto/x509roots/fallback v0.0.0-20240218022100-5bead598a0d4
+	golang.org/x/crypto/x509roots/fallback v0.0.0-20240304182930-7067223927c4
 	golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
-	golang.org/x/net v0.21.0
-	golang.org/x/oauth2 v0.17.0
+	golang.org/x/net v0.22.0
+	golang.org/x/oauth2 v0.18.0
 	golang.org/x/sync v0.6.0
 	golang.org/x/text v0.14.0
-	google.golang.org/grpc v1.62.0
-	google.golang.org/protobuf v1.32.0
+	google.golang.org/grpc v1.62.1
+	google.golang.org/protobuf v1.33.0
 	gopkg.in/yaml.v3 v3.0.1
 	gorm.io/gorm v1.25.7
 	nhooyr.io/websocket v1.8.10
@@ -121,7 +121,7 @@ require (
 	github.com/glebarez/go-sqlite v1.22.0 // indirect
 	github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect
 	github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect
-	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
+	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/go-kit/log v0.2.1 // indirect
 	github.com/go-logfmt/logfmt v0.6.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
@@ -130,7 +130,7 @@ require (
 	github.com/gobwas/pool v0.2.1 // indirect
 	github.com/gobwas/ws v1.3.2 // indirect
 	github.com/golang/mock v1.6.0 // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/gorilla/websocket v1.5.1 // indirect
 	github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect
@@ -165,7 +165,7 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.0 // indirect
-	github.com/prometheus/procfs v0.12.0 // indirect
+	github.com/prometheus/procfs v0.13.0 // indirect
 	github.com/relvacode/iso8601 v1.4.0 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rickb777/date v1.20.6 // indirect
@@ -184,20 +184,20 @@ require (
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/crypto v0.20.0 // indirect
-	golang.org/x/mod v0.15.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
-	golang.org/x/term v0.17.0 // indirect
-	golang.org/x/tools v0.18.0 // indirect
+	golang.org/x/crypto v0.21.0 // indirect
+	golang.org/x/mod v0.16.0 // indirect
+	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/term v0.18.0 // indirect
+	golang.org/x/tools v0.19.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // 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
-	modernc.org/libc v1.41.0 // indirect
+	modernc.org/libc v1.44.1 // indirect
 	modernc.org/mathutil v1.6.0 // indirect
 	modernc.org/memory v1.7.2 // indirect
-	modernc.org/sqlite v1.29.1 // indirect
+	modernc.org/sqlite v1.29.5 // indirect
 )
 
 replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20230705160100-23e1df2c01c3
diff --git a/go.sum b/go.sum
index 54525a1a..e1250beb 100644
--- a/go.sum
+++ b/go.sum
@@ -19,8 +19,8 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj
 github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
-github.com/PuerkitoBio/goquery v1.9.0 h1:zgjKkdpRY9T97Q5DCtcXwfqkcylSFIVCocZmn2huTp8=
-github.com/PuerkitoBio/goquery v1.9.0/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
+github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
+github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
 github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
@@ -54,8 +54,8 @@ github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP
 github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
 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.50.24 h1:3o2Pg7mOoVL0jv54vWtuafoZqAeEXLhm1tltWA2GcEw=
-github.com/aws/aws-sdk-go v1.50.24/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go v1.50.38 h1:h8wxaLin7sFGK4sKassc1VpNcDbgAAEQJ5PHjqLAvXQ=
+github.com/aws/aws-sdk-go v1.50.38/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
 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=
@@ -154,8 +154,8 @@ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcP
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
 github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
-github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
-github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
+github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
+github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
 github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633 h1:ZrzoZQz1CF33SPHLkjRpnVuZwr9cO1lTEc4Js7SgBos=
 github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk=
 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 h1:zga7zaRE8HCbWjcXMDlfvmQtH0/kMVLo7cQ48dy6kWg=
@@ -164,8 +164,8 @@ github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2D
 github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27/go.mod h1:AYvN8omj7nKLmbcXS2dyABYU6JB1Lz1bHmkkq1kf4I4=
 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno=
 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw=
-github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
-github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
+github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
@@ -186,8 +186,8 @@ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/Nu
 github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
-github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
+github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
@@ -213,8 +213,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gokrazy/updater v0.0.0-20240113102150-4ac511a17e33 h1:m6NVAznErzAL9fzaIh+HeFbbImxV6ku4iaUUHhYctmg=
 github.com/gokrazy/updater v0.0.0-20240113102150-4ac511a17e33/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
-github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
-github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -233,8 +233,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -242,11 +242,11 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
@@ -276,10 +276,6 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
 github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
-github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc=
-github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os=
-github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo=
-github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g=
 github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLtw=
 github.com/gregdel/pushover v1.3.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
@@ -314,8 +310,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hasura/go-graphql-client v0.11.0 h1:EFEkpMZlkq5gLZj9oiI6TnHCOHV1oErxOroMc5qUHQI=
-github.com/hasura/go-graphql-client v0.11.0/go.mod h1:eNNnmHAp6NgwKZ4xRbZEfywxr07qk34Y0QhbPsYIfhw=
+github.com/hasura/go-graphql-client v0.12.1 h1:tL+BCoyubkYYyaQ+tJz+oPe/pSxYwOJHwe5SSqqi6WI=
+github.com/hasura/go-graphql-client v0.12.1/go.mod h1:F4N4kR6vY8amio3gEu3tjSZr8GPOXJr3zj72DKixfLE=
 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=
@@ -404,8 +400,8 @@ github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv
 github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc h1:T18nkqs09wcmipKdYNTtN2xT0tvOq+b7bapjD8ZHV1I=
-github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc/go.mod h1:ZynYDWGw6CslG3vyPuucLsy6AyE+h3XXYlr39jhNiQY=
+github.com/lorenzodonini/ocpp-go v0.18.0 h1:XhsKAzrG/1QJym2SYyiwTzr4cOa8geN8qSid3MFuiQ4=
+github.com/lorenzodonini/ocpp-go v0.18.0/go.mod h1:ZynYDWGw6CslG3vyPuucLsy6AyE+h3XXYlr39jhNiQY=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
@@ -541,16 +537,16 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/prometheus-community/pro-bing v0.3.0 h1:SFT6gHqXwbItEDJhTkzPWVqU6CLEtqEfNAPp47RUON4=
-github.com/prometheus-community/pro-bing v0.3.0/go.mod h1:p9dLb9zdmv+eLxWfCT6jESWuDrS+YzpPkQBgysQF8a0=
+github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
+github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
-github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
-github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
+github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
+github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -564,16 +560,16 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
-github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
-github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
+github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
+github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
 github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99 h1:JtoVdxWJ3tgyqtnPq3r4hJ9aULcIDDnPXBWxZsdmqWU=
 github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@@ -674,8 +670,8 @@ github.com/teivah/onecontext v1.3.0/go.mod h1:hoW1nmdPVK/0jrvGtcx8sCKYs2PiS4z0zz
 github.com/teslamotors/vehicle-command v0.0.2 h1:NOoI7d5OVq+Yeaom7iv31sbUMPDhJ/2QRKT4HKHfTa8=
 github.com/teslamotors/vehicle-command v0.0.2/go.mod h1:SydThpjTNvhUXBy1VyiD/A9scQSYcp+E6iElHa36Gk0=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4=
-github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0=
+github.com/traefik/yaegi v0.16.0 h1:PdG291d+XKZnfgfnc9vedZ9DcIrAq4EYTy3/TyESn20=
+github.com/traefik/yaegi v0.16.0/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
 github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
 github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
@@ -684,8 +680,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
 github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
 github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
 github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
-github.com/volkszaehler/mbmd v0.0.0-20231215091549-af16b1f597b9 h1:XhQeU872AXnP13Q3HdAgU0kKtkKw5DxDcac1d4a02ZY=
-github.com/volkszaehler/mbmd v0.0.0-20231215091549-af16b1f597b9/go.mod h1:p1nUKfszvbZ+mMMsMi1vaKGA/eBAXC34VSKCk5slyG4=
+github.com/volkszaehler/mbmd v0.0.0-20240229124119-740c5c8ad344 h1:pxfwqAjssh3n0bz0WcR77sg7twlYdopTjTkBzsjcOSU=
+github.com/volkszaehler/mbmd v0.0.0-20240229124119-740c5c8ad344/go.mod h1:p1nUKfszvbZ+mMMsMi1vaKGA/eBAXC34VSKCk5slyG4=
 github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
 github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@@ -720,15 +716,15 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
-golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
-golang.org/x/crypto/x509roots/fallback v0.0.0-20240218022100-5bead598a0d4 h1:1Tb97Ee8PrjG5+QjtGWEz3J8xiOlM4pmU3EeVWvJ3+s=
-golang.org/x/crypto/x509roots/fallback v0.0.0-20240218022100-5bead598a0d4/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto/x509roots/fallback v0.0.0-20240304182930-7067223927c4 h1:zqDrv4/Ege/1hXnCGw4hQ2mhYKP/pSr/CD31qIJQvvw=
+golang.org/x/crypto/x509roots/fallback v0.0.0-20240304182930-7067223927c4/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
 golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
@@ -743,8 +739,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -773,12 +769,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 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.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
-golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
+golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
+golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -838,15 +835,19 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
-golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -881,8 +882,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
-golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -898,8 +899,8 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 h1:8EeVk1VKMD+GD/neyEHGmz7pFblqPjHoi+PGQIlLx2s=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
 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=
@@ -909,8 +910,8 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
-google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -919,8 +920,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -964,14 +965,30 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
-modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
+modernc.org/cc/v4 v4.19.3 h1:vE9kmJqUcyvNOf8F2Hn8od14SOMq34BiqcZ2tMzLk5c=
+modernc.org/cc/v4 v4.19.3/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/ccgo/v4 v4.10.1 h1:qi+3luLv0LR5UkLmZyKXZxIC4K/136vcAUoYYeGSS+g=
+modernc.org/ccgo/v4 v4.10.1/go.mod h1:9YDnb1IIvHymh899K5a++jza0JIWygZPTc5dlh7xvhQ=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
+modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/libc v1.44.1 h1:dsoFMypkA7AofdQCx4JZpwym4DuWqyzhH1tpyU8ZV5g=
+modernc.org/libc v1.44.1/go.mod h1:RRqfGVjvILF5AdNP3RPCiihj7+Dn2pIBrdlU60lA9vs=
 modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
 modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
 modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
 modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
-modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA=
-modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
+modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
 nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
 pgregory.net/rapid v0.4.7 h1:MTNRktPuv5FNqOO151TM9mDTa+XHcX6ypYeISDVD14g=
diff --git a/i18n/de.toml b/i18n/de.toml
index ae468137..5383391b 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -332,6 +332,7 @@ detectionActive = "Fahrzeugerkennung läuft …"
 fallbackName = "Fahrzeug"
 moreActions = "Weitere Aktionen"
 none = "Kein Fahrzeug"
+notReachable = "Fahrzeug war nicht erreichbar. Versuche evcc neu zu starten."
 targetSoc = "Ladelimit"
 temp = "Temperatur"
 tempLimit = "Zieltemp."
diff --git a/i18n/en.toml b/i18n/en.toml
index 64126f8c..e6719116 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -331,6 +331,7 @@ detectionActive = "Detecting vehicle…"
 fallbackName = "Vehicle"
 moreActions = "More actions"
 none = "No vehicle"
+notReachable = "Vehicle was not reachable. Try restarting evcc."
 targetSoc = "Limit"
 temp = "Temp."
 tempLimit = "Temp. limit"
diff --git a/packaging/scripts/postremove.sh b/packaging/scripts/postremove.sh
index 6ef3b8ce..57e207b9 100644
--- a/packaging/scripts/postremove.sh
+++ b/packaging/scripts/postremove.sh
@@ -18,51 +18,38 @@ if [ "$1" = "purge" ]; then
 	fi
 fi
 
-
-
-
-# if interactive: Call /usr/bin/evcc checkconfig and check the return code (newer version)
+# if interactive: call `/usr/bin/evcc checkconfig` and check the return code (newer version)
 # if return code is 0, do nothing
 # else: Ask user if he wants to keep the old version (working) or the new version (not working) 
 # Remember the choice with /tmp/.evccrollback and fail new-postrm failed-upgrade old-version new-version to initiate dpkg's rollback
-if [ "$1" = "upgrade" ]; then
-	INTERACTIVE=0
-	# is shell script interactive?
-	if [ -t 0 ]; then
-	  INTERACTIVE=1
-	else
-	  INTERACTIVE=0
+if [ "$1" = "upgrade" ] && [ -t 0 ]; then
+	if ! /usr/bin/evcc checkconfig > /dev/null; then
+		echo "--------------------------------------------------------------------------------"
+		echo "ERROR: your configuration is not compatible with the new version:"
+		/usr/bin/evcc checkconfig --log error || true
+		echo "Please consult the release notes: https://github.com/evcc-io/evcc/releases"
+		echo "--------------------------------------------------------------------------------"
+
+		while true; do
+			echo "Do you want to keep your old (working) version? [Y/n]: "
+			read choice
+			case "$choice" in
+				n*|N*|"")
+					echo "We will keep the new version. Your configuration stays untouched!"
+					break
+					;;
+				y*|Y*)
+					echo "The old version will be restored. Your configuration stays untouched! Following errors are intended:"
+					touch /tmp/.evccrollback
+					exit 1
+					break
+					;;
+				*)
+					;;
+			esac
+		done
 	fi
-
-	if [ $INTERACTIVE -eq 1 ]; then
-      if ! /usr/bin/evcc checkconfig > /dev/null; then
-			echo "--------------------------------------------------------------------------------"
-			echo "ERROR: your evcc configuration is not compatible with the new version. Please consider reading the release notes: https://github.com/evcc-io/evcc/releases"
-			echo "checkconfig Output:" 
-			/usr/bin/evcc checkconfig || true
-			echo "--------------------------------------------------------------------------------"
-	
-			while true; do
-				echo "Do you want to keep your old (working) evcc version? [Y/n]: "
-				read choice
-				case "$choice" in
-					n*|N*|"")
-						echo "We will keep the new version. Your evcc configuration stays untouched!"
-						break
-						;;
-					y*|Y*)
-						echo "The old version will be restored. Your evcc configuration stays untouched! Following errors are intended:"
-						touch /tmp/.evccrollback
-						exit 1
-						break
-						;;
-					*)
-						;;
-				esac
-			done
-		fi
-	fi 
-fi
+fi 
 
 # if upgrade goal fails, new-postrm failed-upgrade old-version new-version is called. It should fail to initiate rollback
 if [ "$1" = "failed-upgrade" ]; then
diff --git a/provider/modbus.go b/provider/modbus.go
index fe37a703..b92f55d0 100644
--- a/provider/modbus.go
+++ b/provider/modbus.go
@@ -174,12 +174,20 @@ func (m *Modbus) writeFunc() (func(float64) error, error) {
 	return func(val float64) error {
 		val *= m.scale
 
-		switch {
-		case op.FuncCode == gridx.FuncCodeWriteSingleRegister:
+		switch op.FuncCode {
+		case gridx.FuncCodeWriteSingleCoil:
+			var uval uint16
+			if val != 0 {
+				uval = 0xFF00
+			}
+			_, err = m.conn.WriteSingleCoil(op.Addr, uval)
+			return err
+
+		case gridx.FuncCodeWriteSingleRegister:
 			_, err = m.conn.WriteSingleRegister(op.Addr, uint16(val))
 			return err
 
-		case op.FuncCode == gridx.FuncCodeWriteMultipleRegisters:
+		case gridx.FuncCodeWriteMultipleRegisters:
 			b, err := encode(val)
 			if err == nil {
 				_, err = m.conn.WriteMultipleRegisters(op.Addr, op.Length, b)
diff --git a/provider/mqtt/client.go b/provider/mqtt/client.go
index ca7f02c5..ba3f9718 100644
--- a/provider/mqtt/client.go
+++ b/provider/mqtt/client.go
@@ -74,7 +74,6 @@ func NewClient(log *util.Logger, broker, user, password, clientID string, qos by
 	options.SetOnConnectHandler(mc.ConnectionHandler)
 	options.SetConnectionLostHandler(mc.ConnectionLostHandler)
 	options.SetConnectTimeout(request.Timeout)
-	options.SetOrderMatters(false)
 
 	if insecure {
 		options.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
diff --git a/server/http.go b/server/http.go
index adfdd713..a8b0b3f7 100644
--- a/server/http.go
+++ b/server/http.go
@@ -16,11 +16,18 @@ import (
 )
 
 type route struct {
-	Methods     []string
+	Method      string
 	Pattern     string
 	HandlerFunc http.HandlerFunc
 }
 
+func (r route) Methods() []string {
+	if r.Method == http.MethodGet {
+		return []string{r.Method}
+	}
+	return []string{r.Method, http.MethodOptions}
+}
+
 // HTTPd wraps an http.Server and adds the root router
 type HTTPd struct {
 	*http.Server
@@ -89,55 +96,55 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
 
 	// site api
 	routes := map[string]route{
-		"health":                  {[]string{"GET"}, "/health", healthHandler(site)},
-		"state":                   {[]string{"GET"}, "/state", stateHandler(cache)},
-		"config":                  {[]string{"GET"}, "/config/templates/{class:[a-z]+}", templatesHandler},
-		"products":                {[]string{"GET"}, "/config/products/{class:[a-z]+}", productsHandler},
-		"devices":                 {[]string{"GET"}, "/config/devices/{class:[a-z]+}", devicesHandler},
-		"device":                  {[]string{"GET"}, "/config/devices/{class:[a-z]+}/{id:[0-9.]+}", deviceConfigHandler},
-		"devicestatus":            {[]string{"GET"}, "/config/devices/{class:[a-z]+}/{name:[a-zA-Z0-9_.:-]+}/status", deviceStatusHandler},
-		"site":                    {[]string{"GET"}, "/config/site", siteHandler(site)},
-		"dirty":                   {[]string{"GET"}, "/config/dirty", boolGetHandler(ConfigDirty)},
-		"updatesite":              {[]string{"PUT", "OPTIONS"}, "/config/site", updateSiteHandler(site)},
-		"newdevice":               {[]string{"POST", "OPTIONS"}, "/config/devices/{class:[a-z]+}", newDeviceHandler},
-		"updatedevice":            {[]string{"PUT", "OPTIONS"}, "/config/devices/{class:[a-z]+}/{id:[0-9.]+}", updateDeviceHandler},
-		"deletedevice":            {[]string{"DELETE", "OPTIONS"}, "/config/devices/{class:[a-z]+}/{id:[0-9.]+}", deleteDeviceHandler},
-		"testconfig":              {[]string{"POST", "OPTIONS"}, "/config/test/{class:[a-z]+}", testConfigHandler},
-		"testmerged":              {[]string{"POST", "OPTIONS"}, "/config/test/{class:[a-z]+}/merge/{id:[0-9.]+}", testConfigHandler},
-		"buffersoc":               {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)},
-		"bufferstartsoc":          {[]string{"POST", "OPTIONS"}, "/bufferstartsoc/{value:[0-9.]+}", floatHandler(site.SetBufferStartSoc, site.GetBufferStartSoc)},
-		"batterydischargecontrol": {[]string{"POST", "OPTIONS"}, "/batterydischargecontrol/{value:[a-z]+}", boolHandler(site.SetBatteryDischargeControl, site.GetBatteryDischargeControl)},
-		"prioritysoc":             {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)},
-		"residualpower":           {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
-		"smartcost":               {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[-0-9.]+}", updateSmartCostLimit(site)},
-		"tariff":                  {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
-		"sessions":                {[]string{"GET"}, "/sessions", sessionHandler},
-		"updatesession":           {[]string{"PUT", "OPTIONS"}, "/session/{id:[0-9]+}", updateSessionHandler},
-		"deletesession":           {[]string{"DELETE", "OPTIONS"}, "/session/{id:[0-9]+}", deleteSessionHandler},
-		"telemetry":               {[]string{"GET"}, "/settings/telemetry", boolGetHandler(telemetry.Enabled)},
-		"telemetry2":              {[]string{"POST", "OPTIONS"}, "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)},
+		"health":                  {"GET", "/health", healthHandler(site)},
+		"state":                   {"GET", "/state", stateHandler(cache)},
+		"config":                  {"GET", "/config/templates/{class:[a-z]+}", templatesHandler},
+		"products":                {"GET", "/config/products/{class:[a-z]+}", productsHandler},
+		"devices":                 {"GET", "/config/devices/{class:[a-z]+}", devicesHandler},
+		"device":                  {"GET", "/config/devices/{class:[a-z]+}/{id:[0-9.]+}", deviceConfigHandler},
+		"devicestatus":            {"GET", "/config/devices/{class:[a-z]+}/{name:[a-zA-Z0-9_.:-]+}/status", deviceStatusHandler},
+		"site":                    {"GET", "/config/site", siteHandler(site)},
+		"dirty":                   {"GET", "/config/dirty", boolGetHandler(ConfigDirty)},
+		"updatesite":              {"PUT", "/config/site", updateSiteHandler(site)},
+		"newdevice":               {"POST", "/config/devices/{class:[a-z]+}", newDeviceHandler},
+		"updatedevice":            {"PUT", "/config/devices/{class:[a-z]+}/{id:[0-9.]+}", updateDeviceHandler},
+		"deletedevice":            {"DELETE", "/config/devices/{class:[a-z]+}/{id:[0-9.]+}", deleteDeviceHandler},
+		"testconfig":              {"POST", "/config/test/{class:[a-z]+}", testConfigHandler},
+		"testmerged":              {"POST", "/config/test/{class:[a-z]+}/merge/{id:[0-9.]+}", testConfigHandler},
+		"buffersoc":               {"POST", "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)},
+		"bufferstartsoc":          {"POST", "/bufferstartsoc/{value:[0-9.]+}", floatHandler(site.SetBufferStartSoc, site.GetBufferStartSoc)},
+		"batterydischargecontrol": {"POST", "/batterydischargecontrol/{value:[a-z]+}", boolHandler(site.SetBatteryDischargeControl, site.GetBatteryDischargeControl)},
+		"prioritysoc":             {"POST", "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)},
+		"residualpower":           {"POST", "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
+		"smartcost":               {"POST", "/smartcostlimit/{value:[-0-9.]+}", updateSmartCostLimit(site)},
+		"tariff":                  {"GET", "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
+		"sessions":                {"GET", "/sessions", sessionHandler},
+		"updatesession":           {"PUT", "/session/{id:[0-9]+}", updateSessionHandler},
+		"deletesession":           {"DELETE", "/session/{id:[0-9]+}", deleteSessionHandler},
+		"telemetry":               {"GET", "/settings/telemetry", boolGetHandler(telemetry.Enabled)},
+		"telemetry2":              {"POST", "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)},
 	}
 
 	for _, r := range routes {
-		api.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
+		api.Methods(r.Methods()...).Path(r.Pattern).Handler(r.HandlerFunc)
 	}
 
 	// vehicle api
 	vehicles := map[string]route{
-		"minsoc":   {[]string{"POST", "OPTIONS"}, "/vehicles/{name:[a-zA-Z0-9_.:-]+}/minsoc/{value:[0-9]+}", minSocHandler(site)},
-		"limitsoc": {[]string{"POST", "OPTIONS"}, "/vehicles/{name:[a-zA-Z0-9_.:-]+}/limitsoc/{value:[0-9]+}", limitSocHandler(site)},
-		"plan":     {[]string{"POST", "OPTIONS"}, "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc/{value:[0-9]+}/{time:[0-9TZ:.-]+}", planSocHandler(site)},
-		"plan2":    {[]string{"DELETE", "OPTIONS"}, "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc", planSocRemoveHandler(site)},
+		"minsoc":   {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/minsoc/{value:[0-9]+}", minSocHandler(site)},
+		"limitsoc": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/limitsoc/{value:[0-9]+}", limitSocHandler(site)},
+		"plan":     {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc/{value:[0-9]+}/{time:[0-9TZ:.-]+}", planSocHandler(site)},
+		"plan2":    {"DELETE", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc", planSocRemoveHandler(site)},
 
 		// config ui
-		// "mode":     {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(v)},
-		// "mincurrent": {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9.]+}", floatHandler(pass(v.SetMinCurrent), v.GetMinCurrent)},
-		// "maxcurrent": {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9.]+}", floatHandler(pass(v.SetMaxCurrent), v.GetMaxCurrent)},
-		// "phases":     {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", intHandler(pass(v.SetMinSoc), v.GetMinSoc)},
+		// "mode":       {"POST", "/mode/{value:[a-z]+}", chargeModeHandler(v)},
+		// "mincurrent": {"POST", "/mincurrent/{value:[0-9.]+}", floatHandler(pass(v.SetMinCurrent), v.GetMinCurrent)},
+		// "maxcurrent": {"POST", "/maxcurrent/{value:[0-9.]+}", floatHandler(pass(v.SetMaxCurrent), v.GetMaxCurrent)},
+		// "phases":     {"POST", "/phases/{value:[0-9]+}", intHandler(pass(v.SetMinSoc), v.GetMinSoc)},
 	}
 
 	for _, r := range vehicles {
-		api.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
+		api.Methods(r.Methods()...).Path(r.Pattern).Handler(r.HandlerFunc)
 	}
 
 	// loadpoint api
@@ -145,28 +152,28 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
 		api := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id+1)).Subrouter()
 
 		routes := map[string]route{
-			"mode":             {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", handler(eapi.ChargeModeString, pass(lp.SetMode), lp.GetMode)},
-			"limitsoc":         {[]string{"POST", "OPTIONS"}, "/limitsoc/{value:[0-9]+}", intHandler(pass(lp.SetLimitSoc), lp.GetLimitSoc)},
-			"limitenergy":      {[]string{"POST", "OPTIONS"}, "/limitenergy/{value:[0-9.]+}", floatHandler(pass(lp.SetLimitEnergy), lp.GetLimitEnergy)},
-			"mincurrent":       {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9.]+}", floatHandler(lp.SetMinCurrent, lp.GetMinCurrent)},
-			"maxcurrent":       {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9.]+}", floatHandler(lp.SetMaxCurrent, lp.GetMaxCurrent)},
-			"phases":           {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", intHandler(lp.SetPhases, lp.GetPhases)},
-			"plan":             {[]string{"GET"}, "/plan", planHandler(lp)},
-			"planpreview":      {[]string{"GET"}, "/plan/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.-]+}", planPreviewHandler(lp)},
-			"planenergy":       {[]string{"POST", "OPTIONS"}, "/plan/energy/{value:[0-9.]+}/{time:[0-9TZ:.-]+}", planEnergyHandler(lp)},
-			"planenergy2":      {[]string{"DELETE", "OPTIONS"}, "/plan/energy", planRemoveHandler(lp)},
-			"vehicle":          {[]string{"POST", "OPTIONS"}, "/vehicle/{name:[a-zA-Z0-9_.:-]+}", vehicleSelectHandler(site, lp)},
-			"vehicle2":         {[]string{"DELETE", "OPTIONS"}, "/vehicle", vehicleRemoveHandler(lp)},
-			"vehicleDetect":    {[]string{"PATCH", "OPTIONS"}, "/vehicle", vehicleDetectHandler(lp)},
-			"remotedemand":     {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source:[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
-			"enableThreshold":  {[]string{"POST", "OPTIONS"}, "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)},
-			"disableThreshold": {[]string{"POST", "OPTIONS"}, "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)},
-			"smartCostLimit":   {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[0-9.]+}", floatHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
-			// "priority":         {[]string{"POST", "OPTIONS"}, "/priority/{value:[0-9.]+}", floatHandler(pass(lp.SetPriority), lp.GetPriority)},
+			"mode":             {"POST", "/mode/{value:[a-z]+}", handler(eapi.ChargeModeString, pass(lp.SetMode), lp.GetMode)},
+			"limitsoc":         {"POST", "/limitsoc/{value:[0-9]+}", intHandler(pass(lp.SetLimitSoc), lp.GetLimitSoc)},
+			"limitenergy":      {"POST", "/limitenergy/{value:[0-9.]+}", floatHandler(pass(lp.SetLimitEnergy), lp.GetLimitEnergy)},
+			"mincurrent":       {"POST", "/mincurrent/{value:[0-9.]+}", floatHandler(lp.SetMinCurrent, lp.GetMinCurrent)},
+			"maxcurrent":       {"POST", "/maxcurrent/{value:[0-9.]+}", floatHandler(lp.SetMaxCurrent, lp.GetMaxCurrent)},
+			"phases":           {"POST", "/phases/{value:[0-9]+}", intHandler(lp.SetPhases, lp.GetPhases)},
+			"plan":             {"GET", "/plan", planHandler(lp)},
+			"planpreview":      {"GET", "/plan/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.-]+}", planPreviewHandler(lp)},
+			"planenergy":       {"POST", "/plan/energy/{value:[0-9.]+}/{time:[0-9TZ:.-]+}", planEnergyHandler(lp)},
+			"planenergy2":      {"DELETE", "/plan/energy", planRemoveHandler(lp)},
+			"vehicle":          {"POST", "/vehicle/{name:[a-zA-Z0-9_.:-]+}", vehicleSelectHandler(site, lp)},
+			"vehicle2":         {"DELETE", "/vehicle", vehicleRemoveHandler(lp)},
+			"vehicleDetect":    {"PATCH", "/vehicle", vehicleDetectHandler(lp)},
+			"remotedemand":     {"POST", "/remotedemand/{demand:[a-z]+}/{source:[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
+			"enableThreshold":  {"POST", "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)},
+			"disableThreshold": {"POST", "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)},
+			"smartCostLimit":   {"POST", "/smartcostlimit/{value:[0-9.]+}", floatHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
+			// "priority":         {"POST", "/priority/{value:[0-9.]+}", floatHandler(pass(lp.SetPriority), lp.GetPriority)},
 		}
 
 		for _, r := range routes {
-			api.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
+			api.Methods(r.Methods()...).Path(r.Pattern).Handler(r.HandlerFunc)
 		}
 	}
 }
@@ -185,13 +192,13 @@ func (s *HTTPd) RegisterShutdownHandler(callback func()) {
 
 	// site api
 	routes := map[string]route{
-		"shutdown": {[]string{"POST", "OPTIONS"}, "/shutdown", func(w http.ResponseWriter, r *http.Request) {
+		"shutdown": {"POST", "/shutdown", func(w http.ResponseWriter, r *http.Request) {
 			callback()
 			w.WriteHeader(http.StatusNoContent)
 		}},
 	}
 
 	for _, r := range routes {
-		api.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
+		api.Methods(r.Methods()...).Path(r.Pattern).Handler(r.HandlerFunc)
 	}
 }
diff --git a/server/updater/watch.go b/server/updater/watch.go
index e6978d64..41995d9f 100644
--- a/server/updater/watch.go
+++ b/server/updater/watch.go
@@ -28,7 +28,8 @@ func (u *watch) Send(key string, val interface{}) {
 }
 
 func (u *watch) watchReleases(installed string, out chan *github.RepositoryRelease) {
-	for ; true; <-time.Tick(6 * time.Hour) {
+	tick := time.NewTicker(6 * time.Hour)
+	for ; true; <-tick.C {
 		rel, err := u.findReleaseUpdate(installed)
 		if err != nil {
 			u.log.ERROR.Printf("version check failed: %v (installed: %s)", err, installed)
diff --git a/tariff/amber.go b/tariff/amber.go
index 4c3bc612..83fbb22d 100644
--- a/tariff/amber.go
+++ b/tariff/amber.go
@@ -3,6 +3,7 @@ package tariff
 import (
 	"errors"
 	"fmt"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -13,7 +14,6 @@ import (
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/evcc-io/evcc/util/transport"
-	"golang.org/x/exp/slices"
 )
 
 type Amber struct {
@@ -84,7 +84,8 @@ func (t *Amber) run(done chan error) {
 	var once sync.Once
 	bo := newBackoff()
 
-	for ; true; <-time.Tick(time.Minute) {
+	tick := time.NewTicker(time.Minute)
+	for ; true; <-tick.C {
 		var res []amber.PriceInfo
 
 		if err := backoff.Retry(func() error {
diff --git a/tariff/awattar.go b/tariff/awattar.go
index 2175629d..02344e73 100644
--- a/tariff/awattar.go
+++ b/tariff/awattar.go
@@ -58,7 +58,8 @@ func (t *Awattar) run(done chan error) {
 	bo := newBackoff()
 	client := request.NewHelper(t.log)
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res awattar.Prices
 
 		if err := backoff.Retry(func() error {
diff --git a/tariff/edf-tempo.go b/tariff/edf-tempo.go
index 6ea38b08..812e6f4e 100644
--- a/tariff/edf-tempo.go
+++ b/tariff/edf-tempo.go
@@ -104,7 +104,8 @@ func (t *EdfTempo) run(done chan error) {
 	var once sync.Once
 	bo := newBackoff()
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res struct {
 			Data struct {
 				Values []struct {
diff --git a/tariff/electricitymaps.go b/tariff/electricitymaps.go
index 574923e9..43ccecff 100644
--- a/tariff/electricitymaps.go
+++ b/tariff/electricitymaps.go
@@ -82,7 +82,8 @@ func (t *ElectricityMaps) run(done chan error) {
 	bo := newBackoff()
 	uri := fmt.Sprintf("%s/carbon-intensity/forecast?zone=%s", t.uri, t.zone)
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res CarbonIntensity
 
 		if err := backoff.Retry(func() error {
diff --git a/tariff/elering.go b/tariff/elering.go
index a3d42d9d..7ff5bc73 100644
--- a/tariff/elering.go
+++ b/tariff/elering.go
@@ -62,7 +62,8 @@ func (t *Elering) run(done chan error) {
 	client := request.NewHelper(t.log)
 	bo := newBackoff()
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res elering.NpsPrice
 
 		ts := time.Now().Truncate(time.Hour)
diff --git a/tariff/energinet.go b/tariff/energinet.go
index 8b4f94cc..eac20df9 100644
--- a/tariff/energinet.go
+++ b/tariff/energinet.go
@@ -61,7 +61,8 @@ func (t *Energinet) run(done chan error) {
 	client := request.NewHelper(t.log)
 	bo := newBackoff()
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res energinet.Prices
 
 		ts := time.Now().Truncate(time.Hour)
diff --git a/tariff/entsoe.go b/tariff/entsoe.go
index 7394e156..db2c6340 100644
--- a/tariff/entsoe.go
+++ b/tariff/entsoe.go
@@ -89,7 +89,8 @@ func (t *Entsoe) run(done chan error) {
 	bo := newBackoff()
 
 	// Data updated by ESO every half hour, but we only need data every hour to stay current.
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var tr entsoe.PublicationMarketDocument
 
 		if err := backoff.Retry(func() error {
diff --git a/tariff/groupe-e.go b/tariff/groupe-e.go
index 00a3a486..6c13d953 100644
--- a/tariff/groupe-e.go
+++ b/tariff/groupe-e.go
@@ -27,18 +27,9 @@ func init() {
 }
 
 func NewGroupeEFromConfig(other map[string]interface{}) (api.Tariff, error) {
-	var cc struct {
-		embed `mapstructure:",squash"`
-	}
-
-	if err := util.DecodeOther(other, &cc); err != nil {
-		return nil, err
-	}
-
 	t := &GroupeE{
-		embed: &cc.embed,
-		log:   util.NewLogger("groupe-e"),
-		data:  util.NewMonitor[api.Rates](2 * time.Hour),
+		log:  util.NewLogger("groupe-e"),
+		data: util.NewMonitor[api.Rates](2 * time.Hour),
 	}
 
 	done := make(chan error)
@@ -53,7 +44,8 @@ func (t *GroupeE) run(done chan error) {
 	bo := newBackoff()
 	client := request.NewHelper(t.log)
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res []struct {
 			StartTimestamp time.Time `json:"start_timestamp"`
 			EndTimestamp   time.Time `json:"end_timestamp"`
@@ -82,7 +74,7 @@ func (t *GroupeE) run(done chan error) {
 			ar := api.Rate{
 				Start: r.StartTimestamp.Local(),
 				End:   r.EndTimestamp.Local(),
-				Price: t.totalPrice(r.VarioPlus / 1e2), // Rp/kWh
+				Price: r.VarioPlus / 1e2, // Rp/kWh
 			}
 			data = append(data, ar)
 		}
diff --git a/tariff/gruenstromindex.go b/tariff/gruenstromindex.go
index fa027ce2..abccf695 100644
--- a/tariff/gruenstromindex.go
+++ b/tariff/gruenstromindex.go
@@ -92,7 +92,8 @@ func (t *GrünStromIndex) run(done chan error) {
 	bo := newBackoff()
 	uri := fmt.Sprintf("https://api.corrently.io/v2.0/gsi/prediction?zip=%s", t.zip)
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res gsiForecast
 
 		err := backoff.Retry(func() error {
diff --git a/tariff/ngeso.go b/tariff/ngeso.go
index bd51d472..9b47b8db 100644
--- a/tariff/ngeso.go
+++ b/tariff/ngeso.go
@@ -75,7 +75,8 @@ func (t *Ngeso) run(done chan error) {
 	}
 
 	// Data updated by ESO every half hour, but we only need data every hour to stay current.
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		res, err := backoff.RetryWithData(func() (ngeso.CarbonForecastResponse, error) {
 			res, err := tReq.DoRequest(client)
 			var se request.StatusError
diff --git a/tariff/octopus.go b/tariff/octopus.go
index 35d1c8bd..a1b31835 100644
--- a/tariff/octopus.go
+++ b/tariff/octopus.go
@@ -62,7 +62,8 @@ func (t *Octopus) run(done chan error) {
 	client := request.NewHelper(t.log)
 	bo := newBackoff()
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res octopus.UnitRates
 
 		if err := backoff.Retry(func() error {
diff --git a/tariff/pun.go b/tariff/pun.go
index 2b6eb75a..9cc720a1 100644
--- a/tariff/pun.go
+++ b/tariff/pun.go
@@ -81,7 +81,8 @@ func (t *Pun) run(done chan error) {
 	var once sync.Once
 	bo := newBackoff()
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var today api.Rates
 		if err := backoff.Retry(func() error {
 			var err error
@@ -109,7 +110,7 @@ func (t *Pun) run(done chan error) {
 			continue
 		}
 
-		//merge today and tomorrow data
+		// merge today and tomorrow data
 		data := append(today, tomorrow...)
 		t.data.Set(data)
 
diff --git a/tariff/smartenergy.go b/tariff/smartenergy.go
new file mode 100644
index 00000000..de532ba5
--- /dev/null
+++ b/tariff/smartenergy.go
@@ -0,0 +1,95 @@
+package tariff
+
+import (
+	"slices"
+	"sync"
+	"time"
+
+	"github.com/cenkalti/backoff/v4"
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/tariff/smartenergy"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+)
+
+type SmartEnergy struct {
+	*embed
+	log  *util.Logger
+	data *util.Monitor[api.Rates]
+}
+
+var _ api.Tariff = (*SmartEnergy)(nil)
+
+func init() {
+	registry.Add("smartenergy", NewSmartEnergyFromConfig)
+}
+
+func NewSmartEnergyFromConfig(other map[string]interface{}) (api.Tariff, error) {
+	var cc struct {
+		embed `mapstructure:",squash"`
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	t := &SmartEnergy{
+		embed: &cc.embed,
+		log:   util.NewLogger("smartenergy"),
+		data:  util.NewMonitor[api.Rates](2 * time.Hour),
+	}
+
+	done := make(chan error)
+	go t.run(done)
+	err := <-done
+
+	return t, err
+}
+
+func (t *SmartEnergy) run(done chan error) {
+	var once sync.Once
+	client := request.NewHelper(t.log)
+	bo := newBackoff()
+
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
+		var res smartenergy.Prices
+
+		if err := backoff.Retry(func() error {
+			return client.GetJSON(smartenergy.URI, &res)
+		}, bo); err != nil {
+			once.Do(func() { done <- err })
+
+			t.log.ERROR.Println(err)
+			continue
+		}
+
+		data := make(api.Rates, 0, len(res.Data))
+		for _, r := range res.Data {
+			ar := api.Rate{
+				Start: r.Date.Local(),
+				End:   r.Date.Add(15 * time.Minute).Local(),
+				Price: t.totalPrice(r.Value / 100),
+			}
+			data = append(data, ar)
+		}
+		data.Sort()
+
+		t.data.Set(data)
+		once.Do(func() { close(done) })
+	}
+}
+
+// Rates implements the api.Tariff interface
+func (t *SmartEnergy) Rates() (api.Rates, error) {
+	var res api.Rates
+	err := t.data.GetFunc(func(val api.Rates) {
+		res = slices.Clone(val)
+	})
+	return res, err
+}
+
+// Type implements the api.Tariff interface
+func (t *SmartEnergy) Type() api.TariffType {
+	return api.TariffTypePriceForecast
+}
diff --git a/tariff/smartenergy/types.go b/tariff/smartenergy/types.go
new file mode 100644
index 00000000..f76790ad
--- /dev/null
+++ b/tariff/smartenergy/types.go
@@ -0,0 +1,14 @@
+package smartenergy
+
+import "time"
+
+const URI = "https://apis.smartenergy.at/market/v1/price"
+
+type Prices struct {
+	Data []Price
+}
+
+type Price struct {
+	Date  time.Time
+	Value float64
+}
diff --git a/tariff/tibber.go b/tariff/tibber.go
index c6fe7145..b422ebb4 100644
--- a/tariff/tibber.go
+++ b/tariff/tibber.go
@@ -78,7 +78,8 @@ func (t *Tibber) run(done chan error) {
 		"id": graphql.ID(t.homeID),
 	}
 
-	for ; true; <-time.Tick(time.Hour) {
+	tick := time.NewTicker(time.Hour)
+	for ; true; <-tick.C {
 		var res struct {
 			Viewer struct {
 				Home struct {
diff --git a/templates/definition/charger/menneckes-hcc3.yaml b/templates/definition/charger/mennekes-hcc3.yaml
similarity index 73%
rename from templates/definition/charger/menneckes-hcc3.yaml
rename to templates/definition/charger/mennekes-hcc3.yaml
index 57fdaa79..b9ad1669 100644
--- a/templates/definition/charger/menneckes-hcc3.yaml
+++ b/templates/definition/charger/mennekes-hcc3.yaml
@@ -1,5 +1,5 @@
-template: menneckes-hcc3
-covers: ["amtron"]
+template: mennekes-hcc3
+covers: ["amtron", "menneckes-hcc3"]
 products:
   - brand: Mennekes
     description:
@@ -11,5 +11,5 @@ params:
     choice: ["tcpip"]
     id: 255
 render: |
-  type: menneckes-hcc3
+  type: mennekes-hcc3
   {{- include "modbus" . }}
diff --git "a/templates/definition/charger/weidm\303\274ller.yaml" "b/templates/definition/charger/weidm\303\274ller.yaml"
new file mode 100644
index 00000000..e2e9d84c
--- /dev/null
+++ "b/templates/definition/charger/weidm\303\274ller.yaml"
@@ -0,0 +1,13 @@
+template: weidmüller
+products:
+  - brand: Weidmüller
+    description:
+      generic: AC Smart
+capabilities: ["1p3p", "rfid"]
+requirements:
+  evcc: ["sponsorship"]
+params:
+  - name: host
+render: |
+  type: weidmüller
+  uri: {{ .host }}:502
diff --git a/templates/definition/meter/apsystems-ez1.yaml b/templates/definition/meter/apsystems-ez1.yaml
new file mode 100644
index 00000000..b35eb262
--- /dev/null
+++ b/templates/definition/meter/apsystems-ez1.yaml
@@ -0,0 +1,21 @@
+template: apsystems-ez1
+products:
+  - brand: APsystems
+    description:
+      generic: EZ1
+params:
+  - name: usage
+    choice: ["pv"]
+  - name: host
+render: |
+  type: custom
+  {{- if eq .usage "pv" }}
+  power:
+    source: http
+    uri: http://{{ .host }}:8050/getOutputData
+    jq: .data.p1+.data.p2
+  energy:
+    source: http
+    uri: http://{{ .host }}:8050/getOutputData
+    jq: .data.te1+.data.te2
+  {{- end }}
diff --git a/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml
index 02f37f48..b2c92717 100644
--- a/templates/definition/meter/goodwe-hybrid.yaml
+++ b/templates/definition/meter/goodwe-hybrid.yaml
@@ -3,6 +3,7 @@ products:
   - brand: GoodWe
     description:
       generic: ET/EH/BH/BT Hybrid Inverter
+capabilities: ["battery-control"]
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
@@ -30,7 +31,7 @@ render: |
       address: 36017 # E-Total-Buy
       type: holding
       decode: float32
-    scale: 0.1
+    scale: 0.001
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
@@ -64,7 +65,7 @@ render: |
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 35191 # PV E-Total Total PV Energy
+      address: 35191 # PV Energy-Total
       type: holding
       decode: uint32
     scale: 0.1
@@ -74,23 +75,74 @@ render: |
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 35183 # Pbattery1 First group battery power
+      address: 35182 # Battery1 Power
       type: holding
-      decode: int16
+      decode: int32
   soc:
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 37007 # SOC First group battery capacity
+      address: 37007 # SOC
       type: holding
       decode: uint16
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 35209 # E-BatteryDischarge Discharge energy
+      address: 35209 # Energy-Battery Discharge
       type: holding
       decode: uint32
     scale: 0.1
+  batterymode:
+    source: watchdog
+    timeout: 30s
+    reset: 1 # reset watchdog on normal
+    set:
+      source: switch
+      switch:
+      - case: 1 # normal
+        set:
+          source: const
+          value: 1 # Normal operation mode
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 47511 # EMSPowerMode
+              type: writesingle
+              encoding: uint16
+      - case: 2 # hold
+        set:
+          source: const
+          value: 8 # Battery standby
+          set: 
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 47511 # EMSPowerMode
+              type: writesingle
+              encoding: uint16
+      - case: 3 # charge
+        set:
+          source: sequence
+          set:
+          - source: const
+            value: 2 # Charge from PV+AC according to the EMSPowerSet below
+            set: 
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 47511 # EMSPowerMode
+                type: writesingle
+                encoding: uint16
+          - source: const
+            value: 10000 # For charge battery mode: Maximum allowed power from Grid in W 
+            set: 
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 47512 # EMSPowerSet [0-10000]
+                type: writemultiple
+                encoding: uint16
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/huawei-smartlogger.yaml b/templates/definition/meter/huawei-smartlogger.yaml
index b3854621..cb152018 100644
--- a/templates/definition/meter/huawei-smartlogger.yaml
+++ b/templates/definition/meter/huawei-smartlogger.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Huawei
     description:
       generic: SmartLogger
+capabilities: ["battery-control"]
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
@@ -68,22 +69,21 @@ render: |
   {{- if eq .usage "pv" }}
   power:
     source: modbus
-    {{- include "modbus" . | indent 2 }}
-    timeout: {{ .timeout }}
-    connectdelay: 1s
+    id: 0
+    uri: {{ .host }}:{{ .port }}
     register:
-      address: 32064 # Input power DC (if no battery in your system - for more precise readings use 32080 # Active generation power AC)
+      address: 40521 # Active power
       type: holding
-      decode: int32nan
+      decode: int32
   energy:
     source: modbus
-    {{- include "modbus" . | indent 2 }}
-    timeout: {{ .timeout }}
+    id: 0
+    uri: {{ .host }}:{{ .port }}
     register:
-      address: 32106 # Accumulated energy yield
+      address: 40560 # E-Total
       type: holding
-      decode: uint32nan
-    scale: 0.01
+      decode: uint32
+    scale: 0.1
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
index f6f4f5ce..fc83e117 100644
--- a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
+++ b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Huawei
     description:
       generic: SUN2000 with SDongle & Power Sensor
+capabilities: ["battery-control"]
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
diff --git a/templates/definition/meter/smartfox-em2.yaml b/templates/definition/meter/smartfox-em2.yaml
index 7062502e..705face0 100644
--- a/templates/definition/meter/smartfox-em2.yaml
+++ b/templates/definition/meter/smartfox-em2.yaml
@@ -2,7 +2,7 @@ template: smartfox-em2
 products:
   - brand: Smartfox
     description:
-      generic: Pro, Pro 2, Pro Light, Pro Light 2, Light, Reg, Reg extended (EM2 firmware)
+      generic: Pro, Pro 2, Pro Light, Pro Light 2, Light (EM2 firmware)
 requirements:
   description:
     de: |
diff --git a/templates/definition/meter/smartfox.yaml b/templates/definition/meter/smartfox.yaml
index b79d8fa2..fdf2c8e9 100644
--- a/templates/definition/meter/smartfox.yaml
+++ b/templates/definition/meter/smartfox.yaml
@@ -2,7 +2,7 @@ template: smartfox
 products:
   - brand: Smartfox
     description:
-      generic: Pro, Pro 2, Pro Light, Pro Light 2, Light, Reg, Reg extended
+      generic: Box, Reg, Reg extended
 requirements:
   description:
     de: |
@@ -75,4 +75,9 @@ render: |
     source: http
     uri: {{ include "uri" . }}
     jq: .power_sf
+  energy:
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .day_energy_sf
+    scale: 0.001
   {{- end }}
diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml
index 2b52d0a5..86299576 100644
--- a/templates/definition/meter/solaredge-hybrid.yaml
+++ b/templates/definition/meter/solaredge-hybrid.yaml
@@ -3,14 +3,17 @@ products:
   - brand: SolarEdge
     description:
       generic: Hybrid Inverter
+capabilities: ["battery-control"]
 requirements:
   description:
     de: |
       Nur ein System kann und darf zeitgleich eine Modbus TCP-Verbindung zum Wechselrichter haben!
-      Für die optionale Batteriesteuerung muss StorageConf_CtrlMode (0xE004) auf 4 "Remote" stehen.
+      Für die optionale Batteriesteuerung muss StorageConf_CtrlMode (0xE004) auf 4 "Remote" stehen,
+      sowie der Batteriemodus "Nutzungszeit" aktiv sein.
     en: |
       Only one system can and may have a Modbus TCP connection to the inverter at the same time!
-      For optional battery control, StorageConf_CtrlMode (0xE004) must be set to 4 "Remote".
+      For optional battery control, StorageConf_CtrlMode (0xE004) must be set to 4 "Remote" and
+      battery mode must be set to "Time of Use".
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
diff --git a/templates/definition/tariff/amber.yaml b/templates/definition/tariff/amber.yaml
index a68d6cea..c5787d18 100644
--- a/templates/definition/tariff/amber.yaml
+++ b/templates/definition/tariff/amber.yaml
@@ -1,6 +1,6 @@
 template: amber
 products:
-  - brand: Amber Electric
+  - brand: Amber Electric (AU)
 params:
   - preset: tariff-base
   - name: token
diff --git a/templates/definition/tariff/groupe-e.yaml b/templates/definition/tariff/groupe-e.yaml
index 3e437aa3..a2c288ba 100644
--- a/templates/definition/tariff/groupe-e.yaml
+++ b/templates/definition/tariff/groupe-e.yaml
@@ -1,6 +1,6 @@
 template: groupe-e
 products:
-  - brand: Groupe E
+  - brand: Groupe E (CH)
     description:
       generic: Vario Plus
 params:
diff --git a/templates/definition/tariff/pun.yaml b/templates/definition/tariff/pun.yaml
index a780b282..2c4acda2 100644
--- a/templates/definition/tariff/pun.yaml
+++ b/templates/definition/tariff/pun.yaml
@@ -1,6 +1,6 @@
 template: pun
 products:
-  - brand: PUN Orario
+  - brand: PUN Orario (IT)
 params:
   - preset: tariff-base
 render: |
diff --git a/templates/definition/tariff/smartenergy.yaml b/templates/definition/tariff/smartenergy.yaml
new file mode 100644
index 00000000..1d108306
--- /dev/null
+++ b/templates/definition/tariff/smartenergy.yaml
@@ -0,0 +1,10 @@
+template: smartenergy
+products:
+  - brand: SmartEnergy (AT)
+    description:
+      generic: smartCONTROL
+params:
+  - preset: tariff-base
+render: |
+  type: smartenergy
+  {{ include "tariff-base" . }}
diff --git a/util/cloud/client.go b/util/cloud/client.go
index 84a5ec2a..414da155 100644
--- a/util/cloud/client.go
+++ b/util/cloud/client.go
@@ -3,6 +3,7 @@ package cloud
 import (
 	"crypto/tls"
 	_ "embed"
+	"net"
 
 	"github.com/evcc-io/evcc/util"
 	"google.golang.org/grpc"
@@ -10,7 +11,7 @@ import (
 )
 
 var (
-	host = util.Getenv("GRPC_URI", "sponsor.evcc.io:8080")
+	hostport = util.Getenv("GRPC_URI", "sponsor.evcc.io:8080")
 
 	conn *grpc.ClientConn
 )
@@ -20,9 +21,15 @@ func Connection() (*grpc.ClientConn, error) {
 		return conn, nil
 	}
 
-	var err error
-	creds := credentials.NewTLS(new(tls.Config))
-	conn, err = grpc.Dial(host, grpc.WithTransportCredentials(creds))
+	host, _, err := net.SplitHostPort(hostport)
+	if err != nil {
+		return nil, err
+	}
+
+	creds := credentials.NewTLS(&tls.Config{
+		ServerName: host,
+	})
+	conn, err = grpc.Dial(hostport, grpc.WithTransportCredentials(creds))
 
 	return conn, err
 }
diff --git a/util/modbus/functions.go b/util/modbus/functions.go
index a6fc0f5c..d5c0bc87 100644
--- a/util/modbus/functions.go
+++ b/util/modbus/functions.go
@@ -3,6 +3,7 @@ package modbus
 import (
 	"encoding/binary"
 	"encoding/hex"
+	"errors"
 	"fmt"
 	"strconv"
 	"strings"
@@ -12,6 +13,10 @@ import (
 func decodeMask(mask string) (uint64, error) {
 	mask = strings.ToLower(mask)
 
+	if mask == "" {
+		return 0, errors.New("mask is required")
+	}
+
 	if strings.HasPrefix(mask, "0x") {
 		if len(mask) < 3 {
 			return 0, fmt.Errorf("invalid mask: %s", mask)
diff --git a/util/modbus/modbus.go b/util/modbus/modbus.go
index c71dbc67..d8833e30 100644
--- a/util/modbus/modbus.go
+++ b/util/modbus/modbus.go
@@ -182,8 +182,8 @@ func (mb *Connection) ReadCoils(address, quantity uint16) ([]byte, error) {
 	return mb.ReadCoilsWithSlave(mb.slaveID, address, quantity)
 }
 
-func (mb *Connection) WriteSingleCoil(address, quantity uint16) ([]byte, error) {
-	return mb.WriteSingleCoilWithSlave(mb.slaveID, address, quantity)
+func (mb *Connection) WriteSingleCoil(address, value uint16) ([]byte, error) {
+	return mb.WriteSingleCoilWithSlave(mb.slaveID, address, value)
 }
 
 func (mb *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) {
diff --git a/util/modbus/register.go b/util/modbus/register.go
index 8a5cbcc0..85e27756 100644
--- a/util/modbus/register.go
+++ b/util/modbus/register.go
@@ -51,7 +51,7 @@ func (r Register) Length() (uint16, error) {
 	case strings.Contains(enc, "64"):
 		return 4, nil
 	default:
-		return 0, fmt.Errorf("invalid register encoding: %s", enc)
+		return 0, fmt.Errorf("invalid register length: %s", enc)
 	}
 }
 
@@ -171,6 +171,9 @@ func (r Register) EncodeFunc() (func(float64) ([]byte, error), error) {
 	enc := strings.ToLower(r.encoding())
 
 	switch {
+	case strings.HasPrefix(enc, "bool"):
+		fallthrough
+
 	case strings.HasPrefix(enc, "int") || strings.HasPrefix(enc, "uint"):
 		return r.encodeToBytes(func(v float64) uint64 {
 			return uint64(v)
diff --git a/vehicle/ovms.go b/vehicle/ovms.go
index c4def163..9a292e45 100644
--- a/vehicle/ovms.go
+++ b/vehicle/ovms.go
@@ -6,38 +6,16 @@ import (
 	"net/http"
 	"net/http/cookiejar"
 	"net/url"
-	"strconv"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/provider"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
+	"github.com/evcc-io/evcc/vehicle/ovms"
 	"golang.org/x/net/publicsuffix"
 )
 
-type ovmsStatusResponse struct {
-	Odometer float64 `json:"odometer,string"`
-}
-
-type ovmsChargeResponse struct {
-	ChargeEtrFull    int64   `json:"charge_etr_full,string"`
-	ChargeState      string  `json:"chargestate"`
-	ChargePortOpen   int     `json:"cp_dooropen"`
-	EstimatedRange   string  `json:"estimatedrange"`
-	MessageAgeServer int     `json:"m_msgage_s"`
-	Soc              float64 `json:"soc,string"`
-}
-
-type ovmsLocationResponse struct {
-	Latitude  float64 `json:"latitude,string"`
-	Longitude float64 `json:"longitude,string"`
-}
-
-type ovmsConnectResponse struct {
-	NetConnected int `json:"v_net_connected"`
-}
-
 // OVMS is an api.Vehicle implementation for dexters-web server requests
 type Ovms struct {
 	*embed
@@ -46,9 +24,9 @@ type Ovms struct {
 	vehicleId, server string
 	cache             time.Duration
 	isOnline          bool
-	chargeG           func() (ovmsChargeResponse, error)
-	statusG           func() (ovmsStatusResponse, error)
-	locationG         func() (ovmsLocationResponse, error)
+	chargeG           func() (ovms.ChargeResponse, error)
+	statusG           func() (ovms.StatusResponse, error)
+	locationG         func() (ovms.LocationResponse, error)
 }
 
 func init() {
@@ -107,32 +85,32 @@ func (v *Ovms) uri(path string) string {
 	return fmt.Sprintf("https://%s/api/%s/%s", net.JoinHostPort(v.server, "6869"), path, v.vehicleId)
 }
 
-func (v *Ovms) connectRequest() (ovmsConnectResponse, error) {
-	var res ovmsConnectResponse
+func (v *Ovms) connectRequest() (ovms.ConnectResponse, error) {
+	var res ovms.ConnectResponse
 	err := v.GetJSON(v.uri("vehicle"), &res)
 	return res, err
 }
 
-func (v *Ovms) chargeRequest() (ovmsChargeResponse, error) {
-	var res ovmsChargeResponse
+func (v *Ovms) chargeRequest() (ovms.ChargeResponse, error) {
+	var res ovms.ChargeResponse
 	err := v.GetJSON(v.uri("charge"), &res)
 	return res, err
 }
 
-func (v *Ovms) statusRequest() (ovmsStatusResponse, error) {
-	var res ovmsStatusResponse
+func (v *Ovms) statusRequest() (ovms.StatusResponse, error) {
+	var res ovms.StatusResponse
 	err := v.GetJSON(v.uri("status"), &res)
 	return res, err
 }
 
-func (v *Ovms) locationRequest() (ovmsLocationResponse, error) {
-	var res ovmsLocationResponse
+func (v *Ovms) locationRequest() (ovms.LocationResponse, error) {
+	var res ovms.LocationResponse
 	err := v.GetJSON(v.uri("location"), &res)
 	return res, err
 }
 
 func (v *Ovms) authFlow() error {
-	var resp ovmsConnectResponse
+	var resp ovms.ConnectResponse
 	err := v.loginToServer()
 	if err == nil {
 		resp, err = v.connectRequest()
@@ -144,8 +122,8 @@ func (v *Ovms) authFlow() error {
 }
 
 // batteryAPI provides battery-status api response
-func (v *Ovms) batteryAPI() (ovmsChargeResponse, error) {
-	var resp ovmsChargeResponse
+func (v *Ovms) batteryAPI() (ovms.ChargeResponse, error) {
+	var resp ovms.ChargeResponse
 
 	resp, err := v.chargeRequest()
 	if err != nil {
@@ -164,8 +142,8 @@ func (v *Ovms) batteryAPI() (ovmsChargeResponse, error) {
 }
 
 // statusAPI provides vehicle status api response
-func (v *Ovms) statusAPI() (ovmsStatusResponse, error) {
-	var resp ovmsStatusResponse
+func (v *Ovms) statusAPI() (ovms.StatusResponse, error) {
+	var resp ovms.StatusResponse
 
 	resp, err := v.statusRequest()
 	if err != nil {
@@ -179,8 +157,8 @@ func (v *Ovms) statusAPI() (ovmsStatusResponse, error) {
 }
 
 // location API provides vehicle position api response
-func (v *Ovms) locationAPI() (ovmsLocationResponse, error) {
-	var resp ovmsLocationResponse
+func (v *Ovms) locationAPI() (ovms.LocationResponse, error) {
+	var resp ovms.LocationResponse
 
 	resp, err := v.locationRequest()
 	if err != nil {
@@ -220,15 +198,15 @@ func (v *Ovms) Status() (api.ChargeStatus, error) {
 
 var _ api.VehicleRange = (*Ovms)(nil)
 
+const kmPerMile = 1.609344
+
 // Range implements the api.VehicleRange interface
 func (v *Ovms) Range() (int64, error) {
 	res, err := v.chargeG()
-
-	if err == nil {
-		return strconv.ParseInt(res.EstimatedRange, 0, 64)
+	if res.Units == ovms.UnitMiles {
+		res.EstimatedRange = int64(float64(res.EstimatedRange) * kmPerMile)
 	}
-
-	return 0, err
+	return res.EstimatedRange, err
 }
 
 var _ api.VehicleOdometer = (*Ovms)(nil)
@@ -236,6 +214,9 @@ var _ api.VehicleOdometer = (*Ovms)(nil)
 // Odometer implements the api.VehicleOdometer interface
 func (v *Ovms) Odometer() (float64, error) {
 	res, err := v.statusG()
+	if res.Units == ovms.UnitMiles {
+		res.Odometer *= kmPerMile
+	}
 	return res.Odometer / 10, err
 }
 
diff --git a/vehicle/ovms/types.go b/vehicle/ovms/types.go
new file mode 100644
index 00000000..3de29e46
--- /dev/null
+++ b/vehicle/ovms/types.go
@@ -0,0 +1,27 @@
+package ovms
+
+const UnitMiles = "M"
+
+type StatusResponse struct {
+	Units    string  `json:"units"`
+	Odometer float64 `json:"odometer,string"`
+}
+
+type ChargeResponse struct {
+	Units            string  `json:"units"`
+	ChargeEtrFull    int64   `json:"charge_etr_full,string"`
+	ChargeState      string  `json:"chargestate"`
+	ChargePortOpen   int     `json:"cp_dooropen"`
+	EstimatedRange   int64   `json:"estimatedrange,string"`
+	MessageAgeServer int     `json:"m_msgage_s"`
+	Soc              float64 `json:"soc,string"`
+}
+
+type LocationResponse struct {
+	Latitude  float64 `json:"latitude,string"`
+	Longitude float64 `json:"longitude,string"`
+}
+
+type ConnectResponse struct {
+	NetConnected int `json:"v_net_connected"`
+}
diff --git a/vehicle/vw/id/provider.go b/vehicle/vw/id/provider.go
index f5660ea9..ec62d80e 100644
--- a/vehicle/vw/id/provider.go
+++ b/vehicle/vw/id/provider.go
@@ -149,15 +149,11 @@ var _ api.SocLimiter = (*Provider)(nil)
 // TargetSoc implements the api.SocLimiter interface
 func (v *Provider) TargetSoc() (float64, error) {
 	res, err := v.statusG()
-	if err == nil && res.Charging == nil {
-		err = errors.New("missing charging status")
-	}
-
-	if err == nil {
-		return float64(res.Charging.ChargingSettings.Value.TargetSOCPct), nil
+	if err != nil || res.Charging == nil || res.Charging.ChargingSettings.Value.TargetSOCPct == nil {
+		return 0, api.ErrNotAvailable
 	}
 
-	return 0, err
+	return float64(*res.Charging.ChargingSettings.Value.TargetSOCPct), nil
 }
 
 var _ api.VehicleChargeController = (*Provider)(nil)
diff --git a/vehicle/vw/id/types.go b/vehicle/vw/id/types.go
index a303eb83..00dea23f 100644
--- a/vehicle/vw/id/types.go
+++ b/vehicle/vw/id/types.go
@@ -108,7 +108,7 @@ type Status struct {
 				MaxChargeCurrentAC          string    `json:"maxChargeCurrentAC"` // reduced, maximum
 				AutoUnlockPlugWhenCharged   string    `json:"autoUnlockPlugWhenCharged"`
 				AutoUnlockPlugWhenChargedAC string    `json:"autoUnlockPlugWhenChargedAC"`
-				TargetSOCPct                int       `json:"targetSOC_pct"`
+				TargetSOCPct                *int      `json:"targetSOC_pct"`
 			} `json:"value"`
 		} `json:"chargingSettings"`
 		PlugStatus struct {
diff --git a/vehicle/wrapper.go b/vehicle/wrapper.go
index d92eb0a0..6727185b 100644
--- a/vehicle/wrapper.go
+++ b/vehicle/wrapper.go
@@ -11,11 +11,13 @@ import (
 // Wrapper wraps an api.Vehicle to capture initialization errors
 type Wrapper struct {
 	embed
-	err error
+	typ    string
+	config map[string]interface{}
+	err    error
 }
 
 // NewWrapper creates an offline Vehicle wrapper
-func NewWrapper(name string, other map[string]interface{}, err error) api.Vehicle {
+func NewWrapper(name string, typ string, other map[string]interface{}, err error) api.Vehicle {
 	var cc struct {
 		embed `mapstructure:",squash"`
 		Other map[string]interface{} `mapstructure:",remain"`
@@ -30,11 +32,13 @@ func NewWrapper(name string, other map[string]interface{}, err error) api.Vehicl
 	}
 
 	v := &Wrapper{
-		embed: cc.embed,
-		err:   fmt.Errorf("vehicle not available: %w", err),
+		embed:  cc.embed,
+		typ:    typ,
+		config: cc.Other,
+		err:    fmt.Errorf("vehicle not available: %w", err),
 	}
 
-	v.Features_ = append(v.Features_, api.Offline)
+	v.Features_ = append(v.Features_, api.Offline, api.Retryable)
 	v.SetTitle(cc.Title_)
 
 	return v
@@ -45,11 +49,16 @@ func (v *Wrapper) Error() string {
 	return v.err.Error()
 }
 
+// Error returns the initialization error
+func (v *Wrapper) Config() (string, map[string]interface{}) {
+	return v.typ, v.config
+}
+
 var _ api.Vehicle = (*Wrapper)(nil)
 
 // SetTitle implements the api.TitleSetter interface
 func (v *Wrapper) SetTitle(title string) {
-	v.Title_ = fmt.Sprintf("%s (unavailable)", title)
+	v.Title_ = title
 }
 
 var _ api.Battery = (*Wrapper)(nil)


Test patch:
diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go
index 52e2957a..f2165478 100644
--- a/core/loadpoint_effective_test.go
+++ b/core/loadpoint_effective_test.go
@@ -21,12 +21,16 @@ func TestEffectiveMinMaxCurrent(t *testing.T) {
 		effectiveMin, effectiveMax float64
 	}{
 		{0, 0, 0, 0, 6, 16},
-		{1, 10, 0, 0, 1, 10},     // charger lower
-		{10, 20, 0, 0, 10, 16},   // charger higher - max ignored
-		{0, 0, 1, 10, 6, 10},     // vehicle lower - min ignored
-		{0, 0, 10, 20, 10, 16},   // vehicle higher - max ignored
-		{1, 10, 2, 12, 1, 10},    // charger + vehicle lower
-		{10, 20, 12, 22, 10, 16}, // charger + vehicle higher
+		{2, 0, 0, 0, 2, 16},   // charger min lower, max empty - charger wins
+		{7, 0, 0, 0, 7, 16},   // charger min higher, max empty (no practical use)
+		{0, 10, 0, 0, 6, 10},  // charger max lower, min empty - loadpoint wins
+		{0, 20, 0, 0, 6, 16},  // charger max higher, min empty - loadpoint wins
+		{0, 0, 5, 0, 6, 16},   // vehicle min lower, max empty - loadpoint wins
+		{0, 0, 8, 0, 8, 16},   // vehicle min higher, max empty - vehicle wins
+		{0, 0, 0, 10, 6, 10},  // vehicle max lower, min empty - vehicle wins
+		{0, 0, 0, 20, 6, 16},  // vehicle max higher, min empty - loadpoint wins
+		{2, 0, 5, 0, 5, 16},   // charger + vehicle min lower, max empty - vehicle wins
+		{0, 20, 0, 32, 6, 16}, // charger + vehicle max higher, min empty - loadpoint wins
 	}
 
 	for _, tc := range tc {
@@ -60,7 +64,7 @@ func TestEffectiveMinMaxCurrent(t *testing.T) {
 			lp.vehicle = vehicle
 		}
 
-		assert.Equal(t, tc.effectiveMin, lp.effectiveMinCurrent())
-		assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent())
+		assert.Equal(t, tc.effectiveMin, lp.effectiveMinCurrent(), "min")
+		assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent(), "max")
 	}
 }
diff --git a/tests/vehicle-error.evcc.yaml b/tests/vehicle-error.evcc.yaml
new file mode 100755
index 00000000..bcce474b
--- /dev/null
+++ b/tests/vehicle-error.evcc.yaml
@@ -0,0 +1,54 @@
+interval: 0.1s
+
+site:
+  title: Vehicle Error
+  meters:
+    grid: grid
+
+meters:
+  - name: grid
+    type: custom
+    power:
+      source: js
+      script: |
+        1000
+  - name: charger_meter
+    type: custom
+    power:
+      source: js
+      script: |
+        500
+
+loadpoints:
+  - title: Carport
+    charger: charger
+    meter: charger_meter
+    vehicle: broken_tesla
+    mode: now
+
+chargers:
+  - name: charger
+    type: custom
+    enable:
+      source: js
+      script:
+    enabled:
+      source: js
+      script: |
+        true
+    status:
+      source: js
+      script: |
+        "C"
+    maxcurrent:
+      source: js
+      script: |
+        16
+
+vehicles:
+  - name: broken_tesla
+    type: template
+    template: tesla # not optimal, since real communication with tesla server is happening
+    title: Broken Tesla
+    accessToken: A
+    refreshToken: B
diff --git a/tests/vehicle-error.spec.js b/tests/vehicle-error.spec.js
new file mode 100644
index 00000000..e329d319
--- /dev/null
+++ b/tests/vehicle-error.spec.js
@@ -0,0 +1,29 @@
+const { test, expect } = require("@playwright/test");
+const { start, stop } = require("./evcc");
+
+test.beforeAll(async () => {
+  await start("vehicle-error.evcc.yaml");
+});
+test.afterAll(async () => {
+  await stop();
+});
+
+test.beforeEach(async ({ page }) => {
+  await page.goto("/");
+});
+
+test.describe("vehicle startup error", async () => {
+  test("broken vehicle: normal title and 'not reachable' icon", async ({ page }) => {
+    await expect(page.getByTestId("vehicle-name")).toHaveText("Broken Tesla");
+    await expect(page.getByTestId("vehicle-not-reachable-icon")).toBeVisible();
+  });
+
+  test("guest vehicle: normal title and no icon", async ({ page }) => {
+    // switch to offline vehicle
+    await page.getByRole("button", { name: "Broken Tesla" }).click();
+    await page.getByRole("button", { name: "Guest vehicle" }).click();
+
+    await expect(page.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
+    await expect(page.getByTestId("vehicle-not-reachable-icon")).not.toBeVisible();
+  });
+});