Repository: evcc-io/evcc
Pull request #14581 (2024-06-26 15:23:04)

UI: Charging status icons
fixes https://github.com/evcc-io/evcc/issues/14408
fixes https://github.com/evcc-io/evcc/issues/14057
fixes https://github.com/evcc-io/evcc/issues/12566
fixes https://github.com/evcc-io/evcc/issues/14542

We have a lot of different mechanisms that influence the charging process. Before reasons and states were shown in a single status text. Since only one text could be visible at a time it was quite challenging (or impossible) to always show the most important information for the user. In this PR the status information is divided into two areas: charger status (left) and other settings or environmental factors each with icon+value combination (right). This side deliberately contains no always-visible explanation text to not overload the UI. Tooltips will be added for each entry.

**New behavior**

- 💰🌱 price/co2 limit is always shown if configured
- 🔌 cheap price/co2 availability is shown even if disconnected
-  🚗 vehicle limit is shown when lower than evcc limit (before only as divider line and tooltip)
- ⚠️ plan soc not reachable due to vehicle limit added (before only in plan modal and marker color)
- 🌏 translated text only for charger status, more stable layout even for languages with longer words
- 🌞 ~added solar indicator: session percentage, green = using solar, gray = grid charging, timers replace percentage temporary~  

![Bildschirmfoto 2024-06-26 um 16 54 08](https://github.com/evcc-io/evcc/assets/152287/cd1bc249-2fb3-4cbb-b41d-e0f0e2ce3113)


**TODOs**

- [x] add tooltip with explanation to each icon+value entry
- [x] decide on adding click-actions
- [x] adjust status tests
- [x] add next available smart cost time
- [x] add projected plan end time (maybe separate pr)
- [x] adjust tests

**Video**

[status icons.webm](https://github.com/evcc-io/evcc/assets/152287/d88acb91-37ad-4ce0-bbde-27a85fb3eb23)

**List of all states**

![states](https://github.com/evcc-io/evcc/assets/152287/25a7b1b2-92a5-410e-bd2a-ec54869dc8c9)


VolkerK62: Bin ja hin- und hergerissen.
Einerseits echt klasse, was du dir so einfallen lässt, auf der anderen Seite wird es echt bunt.
Was mir auf den ersten Blick nicht so gut gefällt, dass der Text bzgl enable/disable timer wegfällt 

andig: Ich habe ohne Blick auf die Statusliste versucht, rauszufinden ws Sonne in % oder Zeit bedeutet. Ist mir erstmal nicht gelungen. 
StevieC121176: Ich find es klasse , gefällt mir besser als nur mit dem Text.

naltatis: @VolkerK62 die Bunt-Bedenken hatte ich auch. Gerade wenn man sich die ganzen Kombinationsmöglichkeiten anschaut bekommt man schnell diesen Eindruck. Ich glaube in der Praxis ist das aber deutlich cleaner als es bei der Übersichtsgrafik wirkt. Viele oder Zustände treten nur selten in Kombination auf. Die rot und orange Fälle sind im Betrieb nur die Ausnahme.
naltatis: Ich mag die aktuelle Text Version eigentlich auch ganz gerne. Gerade weil sie nicht erklärungsbedürftig sind. Aber gerade mit der Menge an Feature die wir aktuell bereits haben skaliert das mit den Texten nicht. 
andig: @StevieC121176 hast du es rausgefunden? Schön finde ich es auch- aber mir gehts um den Informationsgehalt. Ein Piktogramm spricht nicht immer mehr als 1000 Worte. 
StevieC121176: > @StevieC121176 hast du es rausgefunden? Schön finde ich es auch- aber mir gehts um den Informationsgehalt. Ein Piktogramm spricht nicht immer mehr als 1000 Worte.
> 
> Wäre Klasse, wenn wir das mit Feintuning richtig sprechend bekommen würden!

Ja, gerade das Sonnensymbol mit den % oder das Herunterregeln auf 1 phasig mit der Zeitangabe z.bsp. finde ich doch sehr aussagekräftig. bis auf einige wenige finde ich das sehr gelungen. 
Evtl könnte man das ja splitten. Bei wenig Platz z.Bsp. in der mobilen Ansichtsvariante könnte man die Symbole und in der Desktopvariante den Text verwenden. 
Ich bin aber auch deiner Meinung, mit etwas Finetuning ist das ne prima Sache.
andig: Was heisst Sonne mit %? Und warum heisst Sonne mit Zeit „disable“? Ich glaube das das aktuell mehr Fragezeichen erzeugen würde. Die Phasenumschaltung impliziert etwas mit hoch/runter, enable/disable kann ich (noch) nicht erkennen. Hab aber auch keine gute Idee 😩
andig: An der Stelle vllt. lieber ein ein/aus Symbol plus Timer? Dass wir im PV Modus sind ergibt ja schon der Modus?
naltatis: > Dass wir im PV Modus sind ergibt ja schon der Modus?

Nicht zwingend. Den PV Modus Namen wollen wir ja ändern. Auch heute ist es ja so, dass man im PV Modus ist, aber der Grund warum geladen wird kann ein anderer sein als PV Überschuss (MinSoC, Plan, smartcost). Daher die Idee das sonnensymbol grün anzuzeigen wenn hier gerade aufgrund von Überschuss geladen wird.

Ich überleg mir noch was um den enable disable Timer besser vom Phase Timer unterscheiden zu können. Wenn ihr Symbolvorschläge habt die nicht hässliche oder mehrdeutig sind gerne her damit. Die Quelle ist hier "Material Symbols".

Sonne + Prozent: in der jetzigen Version (die tooltips kommen noch) wird hier der Anteil Sonnenenergie des aktuellen Ladevorgangs angezeigt. Die gleiche Info die man sich auch oben rechts am Ladepunkt anzeigen lassen und im Ladelog sehen kann. Alternativ hätten wir auch den aktuellen Anteil an Sonnen/Eigenstrom zur Verfügung. Weiß nicht ob das besser wäre.
Wir können die % Zusatzinfo auch weg lassen, dann ist es allerdings kein Icon+wert Paar und das Sonne Icon rutscht ggf. zu nah an das nächste Icon. Daher hier mein Versuch zur Symmetrie. Wir können da auch "aktiv" hinter schreiben aber das hat wenig Informationsgehalt.

@StevieC121176 Längere Beschreibung bei mehr Fläche: könnten wir machen, ist aber nicht einfach. Die Anzahl der icons und damit auch der verfügbare Platz kann sich zur Laufzeit ändern. Einige Sprachen haben wirklich lange Wörter, auch für kurze Texte. Das sehen wir jetzt schon beim aktuellen statustext. Der wird da oft schon mehrzeilig. Daher würd ich hier erst einmal mit kompakt und sprachunabhängig starten wollen. Das ist vorhersagbarer 😄
naltatis: > Und warum heisst Sonne mit Zeit „disable“?

Das wird über den Kontext (lädt/lädt nicht) und die Farbe der eindeutigen.

Grüne Sonne + Timer = pv disable
Graue Sonne + Timer = pv enable

Aber, oben schon geschrieben, ich schau mal nach eine klareren Lösung die auch ohne Kontext/Farbe funktioniert.
VolkerK62: Wie wäre "Change Circle" für Phasentimer
![image](https://github.com/evcc-io/evcc/assets/99199865/ca062970-ef86-4faa-a541-a68ff18f0d1c)

VolkerK62: Enable 
![image](https://github.com/evcc-io/evcc/assets/99199865/852f3258-78df-4491-bffa-9cd8a30e90c7)

Disable
![image](https://github.com/evcc-io/evcc/assets/99199865/7eadcaa7-fb0f-4b4a-9b39-b2a66af425ca)

naltatis: @VolkerK62 Gute Idee. Vielleicht kann ich aus dem Kreis noch ne Sonne machen. Das wäre auf jeden Fall ein gutes Symbol für "Sonne Start" und "Sonne Ende"
premultiply: Ich finde die Farben für enable und disable sind vertauscht.
enable müsste grün sein und disable grau.
naltatis: > Ich finde die Farben für enable und disable sind vertauscht. enable müsste grün sein und disable grau.

Das Konzept hinter den Farben ist, dass die Faktoren farblich grün hervorgehoben sind, die dafür verantwortlich sind, dass aktuell geladen wird.

Das Preislimit ist bpsw. grün, wenn der Preis unterschritten wird und(!) aufgrund dessen auch geladen wird. Im Nicht verbundenen Zustand wird die Unterschreitung angezeigt, aber nicht farblich hervorgehoben (grau).

Analog dazu ist die Idee auch mit dem Sonnen-Icon und seinen Abwandlungen (enable, disable, >3p, >1p). Konzeptionell gehören die alle zusammen: "Laden weil Überschuss". In der Disable Phase wird immer noch geladen (grün), weil es kürzlich noch Überschuss gab. In der Enable Phase wird noch nicht geladen (grau).

Planner: geplante Startzeit (grau), aktives Planladen (grün)

MinSoc ist ne farbliche Ausnahme, weil rot. Stellt aber damit die Verbindung zum roten Ladebalken (wie bisher auch) her.
naltatis: **estimated plan start**

![plan start](https://github.com/evcc-io/evcc/assets/152287/adbca887-18b6-46c6-bb27-fdc51f1ed4ae)

**plan overrun**

![overrun](https://github.com/evcc-io/evcc/assets/152287/524c1d8c-3d6f-4587-ab83-7953b2c812c8)

**plan active + estimated finish time**

![plan active](https://github.com/evcc-io/evcc/assets/152287/63f4ebb6-2dcc-4675-adf4-84bfdaf75457)

VolkerK62: ![image](https://github.com/evcc-io/evcc/assets/99199865/191eaf49-3ef4-460d-be22-5ac267745c6f)

Hier sollte die Sonne aber grün sein, oder?
naltatis: @VolkerK62 ja, korrekt. Ich hab den Sonnen-Zustand (siehe oben) aber jetzt erst einmal raus genommen. Bin mir mit dem Prozentwert noch nicht sicher. Müssen wir in einem späteren PR noch mal diskutieren, was hier eine gute Information ist.
premultiply: Die Prozentanzeige bei der Sonne irritiert mich.
Was sagt diese aus?

Und fehlt nicht noch ein Batteriesymbol 🔋für "battery buffered" (teilweise voll) und "battery start" (voll)?
naltatis: > Die Prozentanzeige bei der Sonne irritiert mich.

Ist aus diesem PR raus. Siehe oben.
naltatis: > Und fehlt nicht noch ein Batteriesymbol 🔋für "battery buffered" (teilweise voll) und "battery start" (voll)?

Ja, diese Zustände (aktuell nicht wirklich im UI erkennbar) sollten wir dann, wenn wir das machen, auch sauber mit abbilden.
andig: Ich gehe grad die Icons durch. Das sieht falsch aus:

<img width="450" alt="Screenshot 2024-06-30 at 12 33 32" src="https://github.com/evcc-io/evcc/assets/184815/5a5c3efe-ffc8-4c56-9079-0ef1f0c28dd2">

Sehr cool:

<img width="1318" alt="Screenshot 2024-06-30 at 12 35 15" src="https://github.com/evcc-io/evcc/assets/184815/67ab172b-2a70-45f1-885d-974eddb5adc7">

Damit werde ich nicht warm- wofür braucht es diese Information? Ist sie nicht auch aus dem Energiediagramm ersichtlich? Oder fällt die je Ladepunkt unterschiedlich aus?

<img width="437" alt="Screenshot 2024-06-30 at 12 36 01" src="https://github.com/evcc-io/evcc/assets/184815/37ca6837-f43e-4a7f-87c2-bbb93ff1bf16">

Das Icon ist schön sprechend, aber auch die Info wird schon im Balkendiagramm angezeigt (mit anderer Farbe). M.E. sollten wir *einen* Mechanismus haben, nciht zwei:

<img width="440" alt="Screenshot 2024-06-30 at 12 37 12" src="https://github.com/evcc-io/evcc/assets/184815/f0450273-7f45-4078-931b-02d4bd306ef5">

Hier haben wir ebenfalls den Mechanismus zweimal:

<img width="1313" alt="Screenshot 2024-06-30 at 12 41 41" src="https://github.com/evcc-io/evcc/assets/184815/cd26cbde-637c-4da6-b54f-9d5a038fc1be">


Hier passt die Beschreibung nicht- das müsste "cheap set" sein, aber nicht "cheap active"?

<img width="453" alt="Screenshot 2024-06-30 at 12 38 42" src="https://github.com/evcc-io/evcc/assets/184815/3a361840-9c0e-48ac-bba5-37f3add6f7fb">

andig: Die Phasenumschaltung finde ich gut erkennbar, ist aber ein anderes Symbol als die Visualisierung der Phasen. Kann man vllt. mit leben.
andig: Bei Co2 und Preis: können wir das reduzieren, indem ein gesetzter (aber nicht aktiver) Preis grau ist (ohne aktuellen Wert), ein aktiver aber grün (ohne gesetzten Wert)?
andig: > Daher die Idee das sonnensymbol grün anzuzeigen wenn hier gerade aufgrund von Überschuss geladen wird.

@naltatis was heisst das dann konkret bzgl. Kriterium Netzanteil == 0? Wofür ist das relevant bzw. auf welches Issue bezieht sich das? Ich finde den connect noch nicht.


andig: Bzgl. Ladeplan scheint es jetzt 2 Mechanismen zu geben, einmal den geplanten Start (aber noch inaktiv) und einmal das voraussichtliche Ende (wenn aktiv). Dann verstehe ich den rechten Screenshot nicht- das müsste doch der Pfeil sein für "geplanter Start":

<img width="825" alt="Screenshot 2024-06-30 at 12 49 23" src="https://github.com/evcc-io/evcc/assets/184815/e6443c7c-0467-41c8-995e-598056f38cc3">

naltatis: Hier noch mal eine aktualisierte Übersicht der Icons und Zustände.

![Bildschirmfoto 2024-06-30 um 23 36 07](https://github.com/evcc-io/evcc/assets/152287/5c137c90-ece5-474d-95fa-689dbe94eaa0)

premultiply: Die grüne Farbe irritiert mich immer noch.
Insbesondere bei Disable und Enable.
Vielleicht das grün dann besser ganz weg?
naltatis: @premultiply ja, jetzt, wo das Sonnen-Icon nicht permanent da und grün ist, wenn wegen PV Überschuss geladen wird, find ich es auch inkonsequent pv disable und phase switch grün zu machen. Daher habe ich diese Zustände jetzt erstmal wieder grau gemacht. Für Smart Cost, Plan und Min Soc habe ich die Farben drin gelassen um hier den aktiven vom inaktiven Zustand besser unterscheiden zu können.

![Bildschirmfoto 2024-07-05 um 11 30 21](https://github.com/evcc-io/evcc/assets/152287/e378d0a6-a393-4d94-8507-75cf6c434b18)

premultiply: Das gefällt mir schon viel besser. 👍🏼

Gäbe jetzt noch pro Indikator konsequent nur ein Symbol vor dem jeweiligen Text hätte es noch eine klarere Line finde ich.
Sprich bei MinSoC das Auto weg und bei der Phasenumschaltung die Sonne weg.
Ggf. dann noch bessere Einzelsymbole finden.
naltatis: All english tooltips

![Bildschirmfoto 2024-07-06 um 01 10 25](https://github.com/evcc-io/evcc/assets/152287/e2684962-1b2a-47f4-ba2d-288e80e12edd)

naltatis: > Ggf. dann noch bessere Einzelsymbole finden.

I'm open for suggestions 😄. Ich fänd ein Icon auch besser. Aber in Zweifelsfall sticht Aussagekraft.
naltatis: @premultiply hier noch mal ein Icon Update. Habe mal ein Low Battery Vehicle Icon erstellt und die Sonne bei der Phase entfernt.

![Bildschirmfoto 2024-07-06 um 01 45 56](https://github.com/evcc-io/evcc/assets/152287/8de39288-2806-4775-a812-587d5ba9ed35)

andig: > <img width="376" alt="Screenshot 2024-07-06 at 10 17 38" src="https://github.com/evcc-io/evcc/assets/184815/5bdb563b-28be-425d-b6b4-073d2ab5b307">

Hier lieber die Zielzeit? Ich würde nicht t+47min oder so in der Art rechnen wollen?
andig: > <img width="378" alt="Screenshot 2024-07-06 at 10 18 52" src="https://github.com/evcc-io/evcc/assets/184815/6b349bc2-76ad-4da4-bbbf-ce58251e59b4">

Sollen wir noch "waiting for vehicle" sagen wenn wir gleichzeitig wissen, dass das Fahrzeug "fertig" hat?
naltatis: > Sollen wir noch "waiting for vehicle" sagen wenn wir gleichzeitig wissen, dass das Fahrzeug "fertig" hat?

Guter Hinweis. Hier ist der Text falsch. Da sollte nur "vehicle Limit" stehen. Reached ist ein anderer Zustand.
naltatis: > Hier lieber die Zielzeit? Ich würde nicht t+47min oder so in der Art rechnen wollen?

Aktuelles Muster: Im Status steht immer die absolute Zeit (8:00 AM) und im Tooltip die relative Zeit von jetzt (in 0:30 h). In diesem konkreten Zustand (overrun) wird die Verspätung gegenüber des Plans angezeigt. Würdest du hier stattdessen auch die relative Zeit von jetzt bis zum Abschluss angeben? Die absolute Zeit steht da ja bereits.
andig: > Aktuelles Muster: Im Status steht immer die absolute Zeit (8:00 AM) und im Tooltip die relative Zeit von jetzt (in 0:30 h)

Ah, hab ich falsch interpretiert- dachte das wäre die ursprüngliche Plan time. Dann passt es perfekt so wie es ist.
All-Ex-U: Das gefällt mir gut!

Die Sonnenauf- und -untergang-Icons finde ich allerdings nicht selbsterklärend. Daher schlage ich vor, statt dessen die Sonne + ein Play bzw. Pause-Icon zu verwenden. Das passt dann zu den Icons bei der Phasen-Reduzierung/Erhöhung. 

Also so:
![image](https://github.com/evcc-io/evcc/assets/55259748/d9794b58-634c-443d-9801-92cb026b4f6b)


andig: > Daher schlage ich vor, statt dessen die Sonne + ein Play bzw. Pause-Icon zu verwenden. 

Die Diskussion ist schon an der Stelle nur 1 Icon zu verwenden.
All-Ex-U: OK. Dann evtl. bei Start/Pause unter der Sonne ein kleines Play/Pause-Symbol in das Icon (statt dem Oben/Unten-Pfeil)?
Und bei geplanter Phasenänderung ein ^ bzw. v unter die Sonne?
Alles in einem Icon. Dann gäbe es eine Unterscheidung: Pfeile für Phasenänderung und Play/Pause für Ladestart/Ladestopp. 
naltatis: @All-Ex-U Danke für die Anregungen. Mit Play/Pause habe ich ganz am Anfang auch schon experimentiert, aber keine gute Kombination mit der Sonne gefunden, die ich ästhetisch fand und die selbsterklärend genug war. Da wir hier jetzt schon einige Runden gedreht haben, würde ich erst mal mit den jetzigen Icons ins Rennen gehen und das Feedback abwarten. Wenn wir merken, dass das erklärungsbedürftig ist, würd ich die Idee noch mal aufgreifen.

@andig Von meiner Seite aus gibts keine Todos mehr hier. Rein damit?

Patch:
diff --git a/assets/js/components/ChargingPlan.vue b/assets/js/components/ChargingPlan.vue
index 3e4060a3..2f25ea8e 100644
--- a/assets/js/components/ChargingPlan.vue
+++ b/assets/js/components/ChargingPlan.vue
@@ -2,16 +2,13 @@
 	<div class="text-center">
 		<LabelAndValue
 			class="root flex-grow-1"
-			:label="title"
+			:label="$t('main.chargingPlan.title')"
 			:class="disabled ? 'opacity-25' : 'opacity-100'"
 			data-testid="charging-plan"
 		>
 			<div class="value m-0 d-block align-items-baseline justify-content-center">
 				<button class="value-button p-0" :class="buttonColor" @click="openModal">
-					<strong v-if="minSocEnabled" class="text-decoration-underline">
-						{{ minSocLabel }}
-					</strong>
-					<strong v-else-if="targetChargeEnabled">
+					<strong v-if="enabled">
 						<span class="targetTimeLabel"> {{ targetTimeLabel() }}</span>
 						<div
 							class="extraValue text-nowrap"
@@ -126,6 +123,7 @@ export default {
 		planEnergy: Number,
 		planTime: String,
 		planTimeUnreachable: Boolean,
+		planOverrun: Number,
 		rangePerSoc: Number,
 		smartCostLimit: Number,
 		smartCostType: String,
@@ -169,27 +167,12 @@ export default {
 			}
 			return [];
 		},
-		targetChargeEnabled: function () {
-			return this.effectivePlanTime;
-		},
 		enabled: function () {
-			return this.targetChargeEnabled || this.minSocEnabled;
-		},
-		minSocLabel: function () {
-			return this.fmtPercentage(this.minSoc);
+			return this.effectivePlanTime;
 		},
 		modalId: function () {
 			return `chargingPlanModal_${this.id}`;
 		},
-		title: function () {
-			if (this.minSocEnabled) {
-				return this.$t("main.chargingPlan.titleMinSoc");
-			}
-			return this.$t("main.chargingPlan.title");
-		},
-		minSocEnabled: function () {
-			return this.minSoc > this.vehicleSoc;
-		},
 		departureTabActive: function () {
 			return this.activeTab === "departure";
 		},
@@ -248,13 +231,14 @@ export default {
 		},
 		openModal() {
 			this.showDeatureTab();
-			if (this.minSocEnabled) {
-				this.showArrivalTab();
-			}
 			this.modal.show();
 		},
-		openPlanModal() {
-			this.showDeatureTab();
+		openPlanModal(arrivalTab = false) {
+			if (arrivalTab) {
+				this.showArrivalTab();
+			} else {
+				this.showDeatureTab();
+			}
 			this.modal.show();
 		},
 		// not computed because it needs to update over time
diff --git a/assets/js/components/ChargingPlanSettings.vue b/assets/js/components/ChargingPlanSettings.vue
index fe55cb68..23a9f585 100644
--- a/assets/js/components/ChargingPlanSettings.vue
+++ b/assets/js/components/ChargingPlanSettings.vue
@@ -61,6 +61,7 @@ export default {
 		capacity: Number,
 		vehicle: Object,
 		vehicleLimitSoc: Number,
+		planOverrun: Number,
 	},
 	emits: ["plan-removed", "plan-updated"],
 	data: function () {
diff --git a/assets/js/components/ChargingPlanWarnings.vue b/assets/js/components/ChargingPlanWarnings.vue
index 48899ba2..619bf863 100644
--- a/assets/js/components/ChargingPlanWarnings.vue
+++ b/assets/js/components/ChargingPlanWarnings.vue
@@ -13,10 +13,10 @@
 			{{ $t("main.targetCharge.costLimitIgnore", { limit: costLimitText }) }}
 		</span>
 		<span v-if="notReachableInTime" class="d-block text-warning mb-1">
-			{{ $t("main.targetCharge.notReachableInTime", { endTime: endTimeFmt }) }}
+			{{ $t("main.targetCharge.notReachableInTime", { overrun: overrunFmt }) }}
 		</span>
-		<span v-if="targetIsAboveVehicleLimit" class="d-block text-danger mb-1">
-			{{ $t("main.targetCharge.targetIsAboveVehicleLimit", { limit: vehicleLimitFmt }) }}
+		<span v-if="targetIsAboveVehicleLimit" class="d-block text-warning mb-1">
+			{{ $t("main.targetCharge.targetIsAboveVehicleLimit") }}
 		</span>
 	</p>
 </template>
@@ -45,6 +45,7 @@ export default {
 		tariff: Object,
 		plan: Object,
 		vehicleLimitSoc: Number,
+		planOverrun: Number,
 	},
 	computed: {
 		endTime: function () {
@@ -54,11 +55,11 @@ export default {
 			const { plan } = this.plan;
 			return plan[plan.length - 1].end;
 		},
-		endTimeFmt: function () {
-			if (!this.endTime) {
+		overrunFmt: function () {
+			if (!this.planOverrun) {
 				return "";
 			}
-			return this.fmtAbsoluteDate(new Date(this.endTime));
+			return this.fmtDuration(this.planOverrun, true, "m");
 		},
 		timeTooFarInTheFuture: function () {
 			if (!this.effectivePlanTime) {
@@ -101,9 +102,6 @@ export default {
 			}
 			return this.fmtKWh(this.limitEnergy * 1e3);
 		},
-		vehicleLimitFmt: function () {
-			return this.fmtSoc(this.vehicleLimitSoc);
-		},
 		goalFmt: function () {
 			if (this.socBasedPlanning) {
 				return this.fmtSoc(this.effectivePlanSoc);
diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue
index 53dda617..abcaf409 100644
--- a/assets/js/components/Loadpoint.vue
+++ b/assets/js/components/Loadpoint.vue
@@ -12,11 +12,15 @@
 						{{ loadpointTitle }}
 					</div>
 				</h3>
-				<LoadpointSettingsButton :id="id" class="d-block d-sm-none" />
+				<LoadpointSettingsButton class="d-block d-sm-none" @click="openSettingsModal" />
 			</div>
 			<div class="mb-3 d-flex align-items-center">
 				<Mode class="flex-grow-1" v-bind="modeProps" @updated="setTargetMode" />
-				<LoadpointSettingsButton :id="id" class="d-none d-sm-block ms-2" />
+				<LoadpointSettingsButton
+					:id="id"
+					class="d-none d-sm-block ms-2"
+					@click="openSettingsModal"
+				/>
 			</div>
 		</div>
 		<LoadpointSettingsModal
@@ -80,6 +84,7 @@
 			@limit-energy-updated="setLimitEnergy"
 			@change-vehicle="changeVehicle"
 			@remove-vehicle="removeVehicle"
+			@open-loadpoint-settings="openSettingsModal"
 		/>
 	</div>
 </template>
@@ -99,6 +104,7 @@ import LoadpointSettingsModal from "./LoadpointSettingsModal.vue";
 import VehicleIcon from "./VehicleIcon";
 import LoadpointSessionInfo from "./LoadpointSessionInfo.vue";
 import smartCostAvailable from "../utils/smartCostAvailable";
+import Modal from "bootstrap/js/dist/modal";
 
 export default {
 	name: "Loadpoint",
@@ -152,6 +158,7 @@ export default {
 		vehicles: Array,
 		planActive: Boolean,
 		planProjectedStart: String,
+		planProjectedEnd: String,
 		planOverrun: Number,
 		planEnergy: Number,
 		planTime: String,
@@ -186,6 +193,7 @@ export default {
 		smartCostLimit: { type: Number, default: 0 },
 		smartCostType: String,
 		smartCostActive: Boolean,
+		smartCostNextStart: String,
 		tariffGrid: Number,
 		tariffCo2: Number,
 		currency: String,
@@ -334,6 +342,12 @@ export default {
 			const inKw = value == 0 || value >= 1000;
 			return this.fmtKWh(value, inKw);
 		},
+		openSettingsModal() {
+			const modal = Modal.getOrCreateInstance(
+				document.getElementById(`loadpointSettingsModal_${this.id}`)
+			);
+			modal.show();
+		},
 	},
 };
 </script>
diff --git a/assets/js/components/LoadpointSettingsButton.vue b/assets/js/components/LoadpointSettingsButton.vue
index 0f762f64..55d6cf94 100644
--- a/assets/js/components/LoadpointSettingsButton.vue
+++ b/assets/js/components/LoadpointSettingsButton.vue
@@ -3,7 +3,6 @@
 		type="button"
 		class="btn btn-sm btn-outline-secondary position-relative border-0 p-2 evcc-gray"
 		data-testid="loadpoint-settings-button"
-		@click="openModal"
 	>
 		<shopicon-regular-adjust size="s"></shopicon-regular-adjust>
 	</button>
@@ -11,20 +10,11 @@
 
 <script>
 import "@h2d2/shopicons/es/regular/adjust";
-import Modal from "bootstrap/js/dist/modal";
 
 export default {
 	name: "LoadpointSettingsButton",
 	props: {
 		id: [String, Number],
 	},
-	methods: {
-		openModal() {
-			const modal = Modal.getOrCreateInstance(
-				document.getElementById(`loadpointSettingsModal_${this.id}`)
-			);
-			modal.show();
-		},
-	},
 };
 </script>
diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue
index 6c19dc24..1c16858b 100644
--- a/assets/js/components/Loadpoints.vue
+++ b/assets/js/components/Loadpoints.vue
@@ -21,9 +21,9 @@
 					:tariffGrid="tariffGrid"
 					:tariffCo2="tariffCo2"
 					:currency="currency"
-					:multiple-loadpoints="loadpoints.length > 1"
-					:grid-configured="gridConfigured"
-					:pv-configured="pvConfigured"
+					:multipleLoadpoints="loadpoints.length > 1"
+					:gridConfigured="gridConfigured"
+					:pvConfigured="pvConfigured"
 					class="h-100"
 					:class="{ 'loadpoint-unselected': !selected(index) }"
 					@click="scrollTo(index)"
diff --git a/assets/js/components/MaterialIcon/Climater.vue b/assets/js/components/MaterialIcon/Climater.vue
new file mode 100644
index 00000000..0b27a8fb
--- /dev/null
+++ b/assets/js/components/MaterialIcon/Climater.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<path
+			fill="currentColor"
+			d="M10.6 22q-1.275 0-1.937-.763T8 19.5q0-.65.288-1.263t.887-1.012q.55-.35.888-.9t.462-1.175l-.3-.15q-.15-.075-.275-.175l-2.3.825q-.425.15-.825.25T6 16q-1.575 0-2.788-1.375T2 10.6q0-1.275.763-1.937T4.475 8q.65 0 1.275.288t1.025.887q.35.55.9.887t1.175.463l.15-.3q.075-.15.175-.275l-.825-2.3q-.15-.425-.25-.825t-.1-.8q0-1.6 1.375-2.813T13.4 2q1.275 0 1.938.763T16 4.475q0 .65-.288 1.275t-.887 1.025q-.55.35-.887.9t-.463 1.175l.3.15q.15.075.275.175l2.3-.85q.425-.15.813-.237T17.975 8Q20 8 21 9.675t1 3.725q0 1.275-.8 1.938T19.425 16q-.625 0-1.213-.288t-.987-.887q-.35-.55-.9-.887t-1.175-.463l-.15.3q-.075.15-.175.275l.825 2.3q.15.4.25.763t.1.762q.025 1.625-1.35 2.875T10.6 22m1.4-8.5q.625 0 1.062-.437T13.5 12t-.437-1.062T12 10.5t-1.062.438T10.5 12t.438 1.063T12 13.5m-1.15-4.8q.15-.05.313-.088t.312-.062q.2-1.05.763-1.95t1.487-1.5q.125-.1.2-.25T14 4.475q0-.2-.15-.337T13.4 4q-.95 0-2.15.413T10 6.025q0 .225.063.425t.112.375zM6 14q.35 0 .825-.175L8.7 13.15q-.05-.15-.088-.313t-.062-.312q-1.05-.2-1.95-.763t-1.5-1.487q-.1-.125-.262-.2T4.475 10q-.225 0-.35.15T4 10.6q0 1.35.513 2.375T6 14m4.6 6q1.175 0 2.313-.475T14 17.875q0-.2-.062-.375t-.113-.325L13.15 15.3q-.15.05-.312.088t-.313.062q-.2 1.05-.763 1.95t-1.487 1.5q-.125.1-.213.263T10 19.5q.025.2.15.35t.45.15m8.825-6q.225 0 .4-.125T20 13.4q0-.95-.4-2.162T17.975 10q-.225 0-.425.05t-.375.1l-1.875.7q.05.15.088.313t.062.312q1.05.2 1.95.763t1.5 1.487q.075.125.225.2t.3.075m-6.9 1.45"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "Climater",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/DynamicPrice.vue b/assets/js/components/MaterialIcon/DynamicPrice.vue
new file mode 100644
index 00000000..7c9a5c7e
--- /dev/null
+++ b/assets/js/components/MaterialIcon/DynamicPrice.vue
@@ -0,0 +1,24 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<g
+			fill="none"
+			stroke="currentColor"
+			stroke-linecap="round"
+			stroke-linejoin="round"
+			stroke-width="2"
+		>
+			<circle cx="8" cy="8" r="6" />
+			<path d="M18.09 10.37A6 6 0 1 1 10.34 18M7 6h1v4" />
+			<path d="m16.71 13.88l.7.71l-2.82 2.82" />
+		</g>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "DynamicPrice",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/PlanEnd.vue b/assets/js/components/MaterialIcon/PlanEnd.vue
new file mode 100644
index 00000000..004a35c2
--- /dev/null
+++ b/assets/js/components/MaterialIcon/PlanEnd.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<path
+			fill="currentColor"
+			d="M21 18q-.425 0-.712-.288T20 17V7q0-.425.288-.712T21 6t.713.288T22 7v10q0 .425-.288.713T21 18m-6.825-5H3q-.425 0-.712-.288T2 12t.288-.712T3 11h11.175L11.3 8.1q-.275-.275-.288-.687T11.3 6.7q.275-.275.7-.275t.7.275l4.6 4.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-4.6 4.6q-.275.275-.687.275T11.3 17.3q-.3-.3-.3-.712t.3-.713z"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "PlanEnd",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/PlanStart.vue b/assets/js/components/MaterialIcon/PlanStart.vue
new file mode 100644
index 00000000..c8c101c0
--- /dev/null
+++ b/assets/js/components/MaterialIcon/PlanStart.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<path
+			fill="currentColor"
+			d="M3 18q-.425 0-.712-.288T2 17V7q0-.425.288-.712T3 6t.713.288T4 7v10q0 .425-.288.713T3 18m15.175-5H7q-.425 0-.712-.288T6 12t.288-.712T7 11h11.175L15.3 8.1q-.275-.275-.288-.687T15.3 6.7q.275-.275.7-.275t.7.275l4.6 4.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-4.6 4.6q-.275.275-.687.275T15.3 17.3q-.3-.3-.3-.712t.3-.713z"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "PlanStart",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/SunDown.vue b/assets/js/components/MaterialIcon/SunDown.vue
new file mode 100644
index 00000000..3736aadc
--- /dev/null
+++ b/assets/js/components/MaterialIcon/SunDown.vue
@@ -0,0 +1,22 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 48 48">
+		<path
+			fill="currentColor"
+			d="M14.216 26.071A9.953 9.953 0 0 1 14 24c0-5.514 4.486-10 10-10s10 4.486 10 10c0 .71-.074 1.403-.216 2.071h-4.152A6.006 6.006 0 0 0 24 18a6.006 6.006 0 0 0-5.632 8.071h-4.152ZM22 4h4v6h-4zM4 22h6v4H4zM38 22h6v4h-6zM15.732 5.68l3 5.196-3.464 2-3-5.196 3.464-2ZM40.321 12.269l2 3.466-5.196 2.999-2-3.465 5.196-3ZM7.679 12.267l5.197 2.999-2 3.466-5.197-3 2-3.465ZM32.269 5.68l3.465 2.001-3 5.195-3.465-2.001 3-5.195Z"
+		/>
+		<path
+			fill="currentColor"
+			d="M26.044 36.261V28h-4.088v8.261l-3.066-3.033L16 36.087 24 44l8-7.913-2.89-2.859-3.066 3.033Z"
+			style="fill-rule: nonzero"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "SunDown",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/SunUp.vue b/assets/js/components/MaterialIcon/SunUp.vue
new file mode 100644
index 00000000..17f36f33
--- /dev/null
+++ b/assets/js/components/MaterialIcon/SunUp.vue
@@ -0,0 +1,22 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 48 48">
+		<path
+			fill="currentColor"
+			d="M14.216 26.071A9.953 9.953 0 0 1 14 24c0-5.514 4.486-10 10-10s10 4.486 10 10c0 .71-.074 1.403-.216 2.071h-4.152A6.006 6.006 0 0 0 24 18a6.006 6.006 0 0 0-5.632 8.071h-4.152ZM22 4h4v6h-4zM4 22h6v4H4zM38 22h6v4h-6zM15.732 5.68l3 5.196-3.464 2-3-5.196 3.464-2ZM40.321 12.269l2 3.466-5.196 2.999-2-3.465 5.196-3ZM7.679 12.267l5.197 2.999-2 3.466-5.197-3 2-3.465ZM32.269 5.68l3.465 2.001-3 5.195-3.465-2.001 3-5.195Z"
+		/>
+		<path
+			fill="currentColor"
+			d="M21.956 35.739V44h4.088v-8.261l3.066 3.033L32 35.913 24 28l-8 7.913 2.89 2.859 3.066-3.033Z"
+			style="fill-rule: nonzero"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "SunUp",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/VehicleLimit.vue b/assets/js/components/MaterialIcon/VehicleLimit.vue
new file mode 100644
index 00000000..4cfd6d61
--- /dev/null
+++ b/assets/js/components/MaterialIcon/VehicleLimit.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<path
+			fill="currentColor"
+			d="M6 19v1q0 .425-.288.713T5 21H4q-.425 0-.712-.288T3 20v-8l2.1-6q.15-.45.538-.725T6.5 5h11q.475 0 .863.275T18.9 6l2.1 6v8q0 .425-.287.713T20 21h-1q-.425 0-.712-.288T18 20v-1zm-.2-9h12.4l-1.05-3H6.85zM5 12v5zm2.5 4q.625 0 1.063-.437T9 14.5t-.437-1.062T7.5 13t-1.062.438T6 14.5t.438 1.063T7.5 16m9 0q.625 0 1.063-.437T18 14.5t-.437-1.062T16.5 13t-1.062.438T15 14.5t.438 1.063T16.5 16M5 17h14v-5H5z"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "VehicleLimit",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/VehicleLimitReached.vue b/assets/js/components/MaterialIcon/VehicleLimitReached.vue
new file mode 100644
index 00000000..8c52e73a
--- /dev/null
+++ b/assets/js/components/MaterialIcon/VehicleLimitReached.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 32 32">
+		<path
+			fill="currentColor"
+			d="M5.333,24l18.667,0l0,-5.433c0.467,-0.067 0.922,-0.167 1.367,-0.3c0.444,-0.134 0.877,-0.3 1.3,-0.5l-0,10.233c-0,0.378 -0.128,0.695 -0.384,0.951c-0.256,0.256 -0.573,0.383 -0.95,0.382l-1.333,0c-0.378,0 -0.694,-0.128 -0.949,-0.384c-0.255,-0.256 -0.383,-0.572 -0.384,-0.949l-0,-1.333l-16,-0l-0,1.333c-0,0.378 -0.128,0.695 -0.384,0.951c-0.256,0.256 -0.573,0.383 -0.95,0.382l-1.333,0c-0.378,0 -0.694,-0.128 -0.949,-0.384c-0.255,-0.256 -0.383,-0.572 -0.384,-0.949l-0,-10.667l2.8,-8c0.133,-0.4 0.372,-0.722 0.717,-0.966c0.345,-0.245 0.728,-0.367 1.149,-0.367l6.1,0c-0.066,0.444 -0.1,0.889 -0.1,1.333c0,0.445 0.034,0.889 0.1,1.334l-5.633,-0l-1.4,4l8.633,-0c0.378,0.533 0.8,1.028 1.267,1.484c0.467,0.456 0.989,0.85 1.567,1.182l-12.534,0l0,6.667Zm15.334,-1.333c-0.552,0.003 -1.024,-0.191 -1.416,-0.583c-0.392,-0.392 -0.587,-0.864 -0.584,-1.417c0.002,-0.553 0.197,-1.025 0.584,-1.416c0.386,-0.391 0.858,-0.586 1.416,-0.584c0.557,0.001 1.029,0.196 1.417,0.584c0.388,0.387 0.582,0.859 0.583,1.416c0.001,0.556 -0.194,1.029 -0.583,1.417c-0.389,0.388 -0.862,0.583 -1.417,0.583Zm-12,-0c-0.552,0.003 -1.024,-0.191 -1.416,-0.583c-0.392,-0.392 -0.587,-0.864 -0.584,-1.417c0.002,-0.553 0.197,-1.025 0.584,-1.416c0.386,-0.391 0.858,-0.586 1.416,-0.584c0.557,0.001 1.029,0.196 1.417,0.584c0.388,0.387 0.582,0.859 0.583,1.416c0.001,0.556 -0.194,1.029 -0.583,1.417c-0.389,0.388 -0.862,0.583 -1.417,0.583Zm13.968,-20.009c1.844,-0 3.417,0.65 4.717,1.95c1.301,1.301 1.95,2.873 1.949,4.716c-0,1.844 -0.651,3.416 -1.95,4.718c-1.3,1.301 -2.872,1.951 -4.716,1.949c-1.845,-0.002 -3.417,-0.652 -4.716,-1.951c-1.3,-1.298 -1.95,-2.87 -1.951,-4.716c-0.001,-1.845 0.649,-3.417 1.951,-4.716c1.301,-1.298 2.873,-1.948 4.716,-1.95Zm-0.934,9.666l4.734,-4.733l-1,-1l-3.734,3.733l-1.866,-1.866l-1,1l2.866,2.866Z"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "VehicleLimitReached",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/VehicleLimitWarning.vue b/assets/js/components/MaterialIcon/VehicleLimitWarning.vue
new file mode 100644
index 00000000..e9e094cc
--- /dev/null
+++ b/assets/js/components/MaterialIcon/VehicleLimitWarning.vue
@@ -0,0 +1,17 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 24 24">
+		<path
+			fill="currentColor"
+			d="M4 18v-5zm-2-5l2.1-6q.15-.45.538-.725T5.5 6h4.575Q10 6.5 10 7t.075 1H5.85L4.8 11h6.475q.425.6.95 1.113T13.4 13H4v5h14v-4.075q.525-.075 1.025-.225t.975-.375V21q0 .425-.288.713T19 22h-1q-.425 0-.712-.288T17 21v-1H5v1q0 .425-.288.713T4 22H3q-.425 0-.712-.288T2 21zm13.5 4q.625 0 1.063-.437T17 15.5t-.437-1.062T15.5 14t-1.062.438T14 15.5t.438 1.063T15.5 17m-9 0q.625 0 1.063-.437T8 15.5t-.437-1.062T6.5 14t-1.062.438T5 15.5t.438 1.063T6.5 17M17 12q-2.075 0-3.537-1.463T12 7q0-2.05 1.45-3.525T17 2q2.075 0 3.538 1.462T22 7t-1.463 3.538T17 12m-.5-4h1V4h-1zm.5 2q.2 0 .35-.15t.15-.35t-.15-.35T17 9t-.35.15t-.15.35t.15.35t.35.15"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "VehicleLimitWarning",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/MaterialIcon/VehicleMinSoc.vue b/assets/js/components/MaterialIcon/VehicleMinSoc.vue
new file mode 100644
index 00000000..346f1d62
--- /dev/null
+++ b/assets/js/components/MaterialIcon/VehicleMinSoc.vue
@@ -0,0 +1,23 @@
+<template>
+	<svg :style="svgStyle" viewBox="0 0 32 32">
+		<path
+			fill="currentColor"
+			d="M8,20.167l0,0.666c0,0.556 -0.194,1.028 -0.583,1.418c-0.388,0.389 -0.861,0.583 -1.417,0.582c-0.556,-0.001 -1.028,-0.195 -1.416,-0.582c-0.388,-0.388 -0.582,-0.86 -0.584,-1.418l0,-9.533c0,-0.156 0.011,-0.311 0.033,-0.467c0.023,-0.155 0.056,-0.3 0.1,-0.433l2.5,-7.1c0.178,-0.533 0.5,-0.967 0.967,-1.3c0.467,-0.333 0.989,-0.5 1.567,-0.5l13.666,-0c0.578,-0 1.1,0.167 1.567,0.5c0.467,0.333 0.789,0.767 0.967,1.3l2.5,7.1c0.044,0.133 0.077,0.278 0.1,0.433c0.022,0.156 0.033,0.311 0.033,0.467l0,9.533c0,0.556 -0.194,1.028 -0.583,1.418c-0.388,0.389 -0.861,0.583 -1.417,0.582c-0.556,-0.001 -1.028,-0.195 -1.416,-0.582c-0.388,-0.388 -0.582,-0.86 -0.584,-1.418l0,-0.666l-16,-0Zm-0.267,-12l16.534,-0l-1.4,-4l-13.734,-0l-1.4,4Zm-1.066,2.666l-0,6.667l-0,-6.667Zm3.333,5.334c0.556,-0 1.028,-0.195 1.417,-0.583c0.39,-0.388 0.584,-0.861 0.583,-1.417c-0.001,-0.557 -0.195,-1.029 -0.583,-1.416c-0.387,-0.388 -0.86,-0.583 -1.417,-0.584c-0.557,-0.002 -1.029,0.193 -1.416,0.584c-0.387,0.391 -0.581,0.863 -0.584,1.416c-0.003,0.553 0.192,1.025 0.584,1.417c0.392,0.392 0.864,0.586 1.416,0.583m12,-0c0.556,-0 1.028,-0.195 1.417,-0.583c0.39,-0.388 0.584,-0.861 0.583,-1.417c-0.001,-0.557 -0.195,-1.029 -0.583,-1.416c-0.387,-0.388 -0.86,-0.583 -1.417,-0.584c-0.557,-0.002 -1.029,0.193 -1.416,0.584c-0.387,0.391 -0.581,0.863 -0.584,1.416c-0.003,0.553 0.192,1.025 0.584,1.417c0.392,0.392 0.864,0.586 1.416,0.583m-15.333,1.333l18.666,0l0,-6.667l-18.666,0l-0,6.667Z"
+			style="fill-rule: nonzero"
+		/>
+		<path
+			fill="currentColor"
+			d="M23.267,25.167l0.666,-0c0.178,-0 0.334,0.066 0.467,0.2c0.133,0.133 0.2,0.289 0.2,0.466l0,1.334c0,0.177 -0.067,0.333 -0.2,0.466c-0.133,0.134 -0.289,0.2 -0.467,0.2l-0.666,0l-0,1.334c-0,0.377 -0.128,0.694 -0.384,0.95c-0.256,0.256 -0.573,0.384 -0.95,0.383l-12,-0c-0.377,-0 -0.694,-0.128 -0.95,-0.384c-0.256,-0.256 -0.384,-0.572 -0.383,-0.949l0,-5.334c0,-0.377 0.128,-0.694 0.384,-0.949c0.256,-0.255 0.572,-0.383 0.949,-0.384l12,-0c0.378,-0 0.695,0.128 0.95,0.384c0.255,0.256 0.383,0.572 0.384,0.949l-0,1.334Zm-12,-0l-0,2.666l-0,-2.666Zm1.54,-0l-0,2.666l7.793,0l0,-2.666l-7.793,-0Z"
+			style="fill-rule: nonzero"
+		/>
+	</svg>
+</template>
+
+<script>
+import icon from "../../mixins/icon";
+
+export default {
+	name: "VehicleMinSoc",
+	mixins: [icon],
+};
+</script>
diff --git a/assets/js/components/Site.vue b/assets/js/components/Site.vue
index 267a6b4a..58a09fdc 100644
--- a/assets/js/components/Site.vue
+++ b/assets/js/components/Site.vue
@@ -101,7 +101,6 @@ export default {
 		uploadProgress: Number,
 		sponsor: { type: Object, default: () => ({}) },
 		smartCostType: String,
-		smartCostActive: Boolean,
 		fatal: Object,
 	},
 	computed: {
diff --git a/assets/js/components/Vehicle.vue b/assets/js/components/Vehicle.vue
index e7c2a3d2..7f8ca75d 100644
--- a/assets/js/components/Vehicle.vue
+++ b/assets/js/components/Vehicle.vue
@@ -6,7 +6,13 @@
 			@change-vehicle="changeVehicle"
 			@remove-vehicle="removeVehicle"
 		/>
-		<VehicleStatus v-bind="vehicleStatus" class="mb-2" />
+		<VehicleStatus
+			v-bind="vehicleStatus"
+			class="mb-2"
+			@open-loadpoint-settings="$emit('open-loadpoint-settings')"
+			@open-minsoc-settings="openMinSocSettings"
+			@open-plan-modal="openPlanModal"
+		/>
 		<VehicleSoc
 			v-bind="vehicleSocProps"
 			class="mt-2 mb-4"
@@ -105,11 +111,15 @@ export default {
 		planActive: Boolean,
 		planEnergy: Number,
 		planProjectedStart: String,
+		planProjectedEnd: String,
 		planTime: String,
 		planTimeUnreachable: Boolean,
+		planOverrun: Number,
 		pvAction: String,
 		pvRemainingInterpolated: Number,
+		sessionSolarPercentage: Number,
 		smartCostActive: Boolean,
+		smartCostNextStart: String,
 		smartCostLimit: Number,
 		smartCostType: String,
 		socBasedCharging: Boolean,
@@ -125,7 +135,13 @@ export default {
 		vehicleLimitSoc: Number,
 		vehicleNotReachable: Boolean,
 	},
-	emits: ["limit-soc-updated", "limit-energy-updated", "change-vehicle", "remove-vehicle"],
+	emits: [
+		"limit-soc-updated",
+		"limit-energy-updated",
+		"change-vehicle",
+		"remove-vehicle",
+		"open-loadpoint-settings",
+	],
 	data() {
 		return {
 			displayLimitSoc: this.effectiveLimitSoc,
@@ -202,6 +218,9 @@ export default {
 			}
 			return false;
 		},
+		smartCostDisabled: function () {
+			return ["off", "now"].includes(this.mode);
+		},
 	},
 	watch: {
 		effectiveLimitSoc: function () {
@@ -232,6 +251,9 @@ export default {
 		openPlanModal() {
 			this.$refs.chargingPlan.openPlanModal();
 		},
+		openMinSocSettings() {
+			this.$refs.chargingPlan.openPlanModal(true);
+		},
 	},
 };
 </script>
diff --git a/assets/js/components/VehicleSoc.vue b/assets/js/components/VehicleSoc.vue
index 2f7be442..2484dc1a 100644
--- a/assets/js/components/VehicleSoc.vue
+++ b/assets/js/components/VehicleSoc.vue
@@ -42,10 +42,6 @@
 				v-show="planMarkerAvailable"
 				class="plan-marker"
 				data-bs-toggle="tooltip"
-				:class="{
-					'plan-marker--warning': planTimeUnreachable,
-					'plan-marker--error': planMarkerUnreachable,
-				}"
 				:style="{ left: `${planMarkerPosition}%` }"
 				data-testid="plan-marker"
 				@click="$emit('plan-clicked')"
@@ -92,7 +88,6 @@ export default {
 		effectiveLimitSoc: Number,
 		limitEnergy: Number,
 		planEnergy: Number,
-		planTimeUnreachable: Boolean,
 		chargedEnergy: Number,
 		socBasedCharging: Boolean,
 		socBasedPlanning: Boolean,
@@ -153,13 +148,6 @@ export default {
 			}
 			return this.planMarkerPosition > 0;
 		},
-		planMarkerUnreachable: function () {
-			if (this.socBasedPlanning) {
-				const vehicleLimit = this.vehicleLimitSoc || 100;
-				return this.effectivePlanSoc > vehicleLimit;
-			}
-			return false;
-		},
 		energyLimitMarkerPosition: function () {
 			if (this.socBasedCharging) {
 				return null;
@@ -392,19 +380,6 @@ export default {
 	background-color: var(--evcc-darker-green);
 	transition: background-color var(--evcc-transition-fast) linear;
 }
-.plan-marker--warning {
-	color: var(--bs-warning);
-}
-.plan-marker--warning::before {
-	background-color: var(--bs-warning);
-}
-.plan-marker--error {
-	opacity: 1;
-	color: var(--bs-danger);
-}
-.plan-marker--error::before {
-	background-color: var(--bs-danger);
-}
 .energy-limit-marker {
 	position: absolute;
 	top: -4px;
diff --git a/assets/js/components/VehicleStatus.story.vue b/assets/js/components/VehicleStatus.story.vue
new file mode 100644
index 00000000..8c1dacf1
--- /dev/null
+++ b/assets/js/components/VehicleStatus.story.vue
@@ -0,0 +1,236 @@
+<script setup>
+import VehicleStatus from "./VehicleStatus.vue";
+
+function getFutureTime(hours, minutes) {
+	const now = new Date();
+	now.setHours(now.getHours() + hours);
+	now.setMinutes(now.getMinutes() + minutes);
+	return now.toISOString();
+}
+
+const planProjectedStart = getFutureTime(3, 21);
+const effectivePlanTime = getFutureTime(6, 54);
+const planProjectedEnd = getFutureTime(5, 43);
+</script>
+
+<template>
+	<Story title="VehicleStatus" :layout="{ type: 'grid', iframe: false, width: 420 }">
+		<Variant title="status: disconnected">
+			<VehicleStatus />
+		</Variant>
+		<Variant title="status: connected">
+			<VehicleStatus connected />
+		</Variant>
+		<Variant title="status: enabled">
+			<VehicleStatus connected enabled />
+		</Variant>
+		<Variant title="status: charging">
+			<VehicleStatus connected charging />
+		</Variant>
+		<Variant title="solar: pv enable">
+			<VehicleStatus connected pvAction="enable" :pvRemainingInterpolated="90" />
+		</Variant>
+		<Variant title="solar: charging">
+			<VehicleStatus connected enabled charging :sessionSolarPercentage="94" />
+		</Variant>
+		<Variant title="solar: pv disable">
+			<VehicleStatus connected charging pvAction="disable" :pvRemainingInterpolated="90" />
+		</Variant>
+		<Variant title="solar: pv reduce phases">
+			<VehicleStatus
+				connected
+				charging
+				phaseAction="scale1p"
+				:phaseRemainingInterpolated="181"
+			/>
+		</Variant>
+		<Variant title="solar: pv increase phases">
+			<VehicleStatus
+				connected
+				charging
+				phaseAction="scale3p"
+				:phaseRemainingInterpolated="44"
+			/>
+		</Variant>
+		<Variant title="min soc">
+			<VehicleStatus connected charging :minSoc="20" :vehicleSoc="10" />
+		</Variant>
+		<Variant title="plan: start soon">
+			<VehicleStatus
+				connected
+				:effectivePlanTime="effectivePlanTime"
+				:planProjectedStart="planProjectedStart"
+			/>
+		</Variant>
+		<Variant title="plan: start soon, charging">
+			<VehicleStatus
+				connected
+				charging
+				:effectivePlanTime="effectivePlanTime"
+				:planProjectedStart="planProjectedStart"
+			/>
+		</Variant>
+		<Variant title="plan: active">
+			<VehicleStatus
+				connected
+				charging
+				planActive
+				:effectivePlanTime="effectivePlanTime"
+				:planProjectedStart="planProjectedStart"
+				:planProjectedEnd="planProjectedEnd"
+			/>
+		</Variant>
+		<Variant title="plan: active, not reachable">
+			<VehicleStatus
+				connected
+				charging
+				planActive
+				:effectivePlanTime="effectivePlanTime"
+				:planProjectedStart="planProjectedStart"
+				:planProjectedEnd="planProjectedEnd"
+				:planOverrun="1829"
+				planTimeUnreachable
+			/>
+		</Variant>
+		<Variant title="vehicle: climating">
+			<VehicleStatus connected enabled vehicleClimaterActive />
+		</Variant>
+		<Variant title="vehicle: limit">
+			<VehicleStatus connected enabled :vehicleSoc="33" :vehicleLimitSoc="70" />
+		</Variant>
+		<Variant title="vehicle: limit reached">
+			<VehicleStatus connected enabled :vehicleSoc="70" :vehicleLimitSoc="70" />
+		</Variant>
+		<Variant title="vehicle: limit unreachable">
+			<VehicleStatus
+				connected
+				enabled
+				:vehicleSoc="40"
+				:vehicleLimitSoc="70"
+				:effectivePlanSoc="80"
+			/>
+		</Variant>
+		<Variant title="smart cost: clean energy set">
+			<VehicleStatus
+				connected
+				enabled
+				charging
+				:tariffCo2="600"
+				:smartCostLimit="500"
+				smartCostType="co2"
+			/>
+		</Variant>
+		<Variant title="smart cost: clean energy next start">
+			<VehicleStatus
+				connected
+				enabled
+				charging
+				:smartCostLimit="500"
+				smartCostType="co2"
+				:smartCostNextStart="planProjectedStart"
+			/>
+		</Variant>
+		<Variant title="smart cost: clean energy active">
+			<VehicleStatus
+				connected
+				enabled
+				charging
+				:tariffCo2="400"
+				:smartCostLimit="500"
+				smartCostType="co2"
+				smartCostActive
+			/>
+		</Variant>
+		<Variant title="smart cost: cheap energy set">
+			<VehicleStatus
+				connected
+				enabled
+				charging
+				currency="CHF"
+				:tariffGrid="0.32"
+				:smartCostLimit="0.12"
+				smartCostType="price"
+			/>
+		</Variant>
+		<Variant title="smart cost: cheap but not connected">
+			<VehicleStatus
+				currency="EUR"
+				:tariffGrid="0.091"
+				:smartCostLimit="0.12"
+				smartCostType="price"
+			/>
+		</Variant>
+		<Variant title="smart cost: cheap energy next start">
+			<VehicleStatus
+				connected
+				enabled
+				charging
+				currency="CHF"
+				:smartCostLimit="0.12"
+				smartCostType="price"
+				:smartCostNextStart="planProjectedStart"
+			/>
+		</Variant>
+		<Variant title="smart cost: cheap energy active">
+			<VehicleStatus
+				connected
+				enabled
+				charging
+				currency="CHF"
+				:tariffGrid="0.11"
+				:smartCostLimit="0.12"
+				smartCostType="price"
+				smartCostActive
+			/>
+		</Variant>
+		<Variant title="combination: minsoc, cheap">
+			<VehicleStatus
+				connected
+				enabled
+				charging
+				currency="EUR"
+				:smartCostLimit="0.15"
+				smartCostType="price"
+				:minSoc="20"
+				:vehicleSoc="10"
+			/>
+		</Variant>
+		<Variant title="combination: pv disable, plan">
+			<VehicleStatus
+				connected
+				charging
+				pvAction="disable"
+				:pvRemainingInterpolated="181"
+				:effectivePlanTime="effectivePlanTime"
+				:planProjectedStart="planProjectedStart"
+			/>
+		</Variant>
+		<Variant title="combination: vehicle limit, plan">
+			<VehicleStatus
+				connected
+				charging
+				:sessionSolarPercentage="94"
+				:vehicleLimitSoc="80"
+				:effectiveLimitSoc="90"
+				:effectivePlanTime="effectivePlanTime"
+				:planProjectedStart="planProjectedStart"
+			/>
+		</Variant>
+		<Variant title="combination: maximal">
+			<VehicleStatus
+				connected
+				charging
+				:sessionSolarPercentage="94"
+				:minSoc="20"
+				:vehicleSoc="10"
+				:tariffGrid="0.33"
+				:smartCostLimit="0.2"
+				smartCostType="price"
+				:smartCostNextStart="planProjectedStart"
+				:effectivePlanTime="effectivePlanTime"
+				:planProjectedStart="planProjectedStart"
+				vehicleClimaterActive
+			/>
+		</Variant>
+	</Story>
+</template>
diff --git a/assets/js/components/VehicleStatus.vue b/assets/js/components/VehicleStatus.vue
index f05fd567..c5718dfa 100644
--- a/assets/js/components/VehicleStatus.vue
+++ b/assets/js/components/VehicleStatus.vue
@@ -1,39 +1,238 @@
 <template>
-	<div class="d-block evcc-gray" data-testid="vehicle-status">{{ message }}&nbsp;</div>
+	<div
+		class="d-flex justify-content-between gap-4 evcc-gray"
+		style="min-height: 24px"
+		data-testid="vehicle-status"
+	>
+		<div class="text-nowrap" data-testid="vehicle-status-charger">{{ chargerStatus }}</div>
+		<div class="d-flex flex-wrap justify-content-end gap-3">
+			<!-- pv/phase timer -->
+			<div
+				v-if="pvTimerVisible"
+				ref="pvTimer"
+				class="entry"
+				data-testid="vehicle-status-pvtimer"
+				data-bs-toggle="tooltip"
+			>
+				<SunUpIcon v-if="pvAction === 'enable'" />
+				<SunDownIcon v-else />
+				<div class="tabular">{{ fmtDuration(pvRemainingInterpolated) }}</div>
+			</div>
+			<div
+				v-else-if="phaseTimerVisible"
+				ref="phaseTimer"
+				class="entry"
+				data-testid="vehicle-status-phasetimer"
+				data-bs-toggle="tooltip"
+			>
+				<shopicon-regular-angledoublerightsmall
+					:class="phaseIconClass"
+				></shopicon-regular-angledoublerightsmall>
+				<div class="tabular">{{ fmtDuration(phaseRemainingInterpolated) }}</div>
+			</div>
+
+			<!-- vehicle -->
+			<button
+				v-if="minSocVisible"
+				ref="minSoc"
+				type="button"
+				class="entry text-danger text-decoration-underline"
+				data-testid="vehicle-status-minsoc"
+				data-bs-toggle="tooltip"
+				@click="openMinSocSettings"
+			>
+				<VehicleMinSocIcon />
+				{{ fmtPercentage(minSoc) }}
+			</button>
+			<div
+				v-else-if="vehicleLimitVisible"
+				ref="vehicleLimit"
+				class="entry"
+				:class="vehicleLimitClass"
+				data-bs-toggle="tooltip"
+				data-testid="vehicle-status-limit"
+				:role="vehicleLimitWarning ? 'button' : null"
+				@click="vehicleLimitClicked"
+			>
+				<component :is="vehicleLimitIconComponent" />
+				{{ fmtPercentage(vehicleLimitSoc) }}
+			</div>
+			<div
+				v-if="vehicleClimaterActive"
+				ref="vehicleClimater"
+				data-bs-toggle="tooltip"
+				class="entry"
+				data-testid="vehicle-status-climater"
+			>
+				<ClimaterIcon />
+			</div>
+
+			<!-- smart cost -->
+			<button
+				v-if="smartCostVisible"
+				ref="smartCost"
+				type="button"
+				class="entry"
+				:class="smartCostClass"
+				data-testid="vehicle-status-smartcost"
+				data-bs-toggle="tooltip"
+				@click="smartCostClicked"
+			>
+				<DynamicPriceIcon v-if="smartCostPrice" />
+				<shopicon-regular-eco1 v-else></shopicon-regular-eco1>
+				<div>
+					<span v-if="smartCostNowVisible">{{ smartCostNow }}</span>
+					≤ <span class="text-decoration-underline">{{ smartCostLimitFmt }}</span>
+					<span v-if="smartCostNextStart">
+						({{ fmtAbsoluteDate(new Date(smartCostNextStart)) }})
+					</span>
+				</div>
+			</button>
+
+			<!-- plan -->
+			<button
+				v-if="planActiveVisible"
+				ref="planActive"
+				type="button"
+				class="entry"
+				:class="planActiveClass"
+				data-testid="vehicle-status-planactive"
+				data-bs-toggle="tooltip"
+				@click="planActiveClicked"
+			>
+				<PlanEndIcon />
+				<span>
+					{{ fmtAbsoluteDate(new Date(planProjectedEnd)) }}
+				</span>
+			</button>
+			<button
+				v-else-if="planStartVisible"
+				ref="planStart"
+				type="button"
+				class="entry"
+				data-testid="vehicle-status-planstart"
+				data-bs-toggle="tooltip"
+				@click="planStartClicked"
+			>
+				<PlanStartIcon />
+				{{ fmtAbsoluteDate(new Date(planProjectedStart)) }}
+			</button>
+		</div>
+	</div>
 </template>
 
 <script>
+import "@h2d2/shopicons/es/regular/sun";
+import "@h2d2/shopicons/es/regular/eco1";
+import "@h2d2/shopicons/es/regular/angledoublerightsmall";
+import "@h2d2/shopicons/es/regular/clock";
+import DynamicPriceIcon from "./MaterialIcon/DynamicPrice.vue";
 import { DEFAULT_LOCALE } from "../i18n";
 import formatter from "../mixins/formatter";
 import { CO2_TYPE } from "../units";
+import PlanStartIcon from "./MaterialIcon/PlanStart.vue";
+import PlanEndIcon from "./MaterialIcon/PlanEnd.vue";
+import ClimaterIcon from "./MaterialIcon/Climater.vue";
+import VehicleLimitReachedIcon from "./MaterialIcon/VehicleLimitReached.vue";
+import VehicleLimitWarningIcon from "./MaterialIcon/VehicleLimitWarning.vue";
+import VehicleLimitIcon from "./MaterialIcon/VehicleLimit.vue";
+import VehicleMinSocIcon from "./MaterialIcon/VehicleMinSoc.vue";
+import SunDownIcon from "./MaterialIcon/SunDown.vue";
+import SunUpIcon from "./MaterialIcon/SunUp.vue";
+import Tooltip from "bootstrap/js/dist/tooltip";
 
 export default {
 	name: "VehicleStatus",
+	components: {
+		DynamicPriceIcon,
+		PlanStartIcon,
+		PlanEndIcon,
+		ClimaterIcon,
+		VehicleLimitIcon,
+		VehicleMinSocIcon,
+		SunDownIcon,
+		SunUpIcon,
+	},
 	mixins: [formatter],
 	props: {
 		vehicleSoc: Number,
-		vehicleLimitSoc: Number,
-		minSoc: Number,
-		enabled: Boolean,
-		connected: Boolean,
 		charging: Boolean,
-		heating: Boolean,
-		effectivePlanTime: String,
-		planProjectedStart: String,
 		chargingPlanDisabled: Boolean,
-		planActive: Boolean,
+		connected: Boolean,
+		currency: String,
+		effectiveLimitSoc: Number,
+		effectivePlanSoc: Number,
+		effectivePlanTime: String,
+		enabled: Boolean,
+		heating: Boolean,
+		minSoc: Number,
 		phaseAction: String,
 		phaseRemainingInterpolated: Number,
+		planActive: Boolean,
+		planOverrun: Number,
+		planProjectedEnd: String,
+		planProjectedStart: String,
+		planTimeUnreachable: Boolean,
 		pvAction: String,
 		pvRemainingInterpolated: Number,
-		targetChargeDisabled: Boolean,
-		vehicleClimaterActive: Boolean,
+		smartCostActive: Boolean,
+		smartCostDisabled: Boolean,
 		smartCostLimit: Number,
+		smartCostNextStart: String,
 		smartCostType: String,
-		smartCostActive: Boolean,
-		tariffGrid: Number,
 		tariffCo2: Number,
-		currency: String,
+		tariffGrid: Number,
+		vehicleClimaterActive: Boolean,
+		vehicleLimitSoc: Number,
+	},
+	emits: ["open-loadpoint-settings", "open-minsoc-settings", "open-plan-modal"],
+	data() {
+		return {
+			pvTooltip: null,
+			phaseTooltip: null,
+			planStartTooltip: null,
+			planActiveTooltip: null,
+			minSocTooltip: null,
+			vehicleClimaterTooltip: null,
+			smartCostTooltip: null,
+			vehicleLimitTooltip: null,
+		};
+	},
+	mounted() {
+		this.updatePlanStartTooltip();
+		this.updatePlanActiveTooltip();
+		this.updateMinSocTooltip();
+		this.updatePhaseTooltip();
+		this.updatePvTooltip();
+		this.updateVehicleClimaterTooltip();
+		this.updateSmartCostTooltip();
+		this.updateVehicleLimitTooltip();
+	},
+	watch: {
+		planActiveTooltipContent() {
+			this.$nextTick(this.updatePlanActiveTooltip);
+		},
+		planStartTooltipContent() {
+			this.$nextTick(this.updatePlanStartTooltip);
+		},
+		minSocTooltipContent() {
+			this.$nextTick(this.updateMinSocTooltip);
+		},
+		phaseTimerContent() {
+			this.$nextTick(this.updatePhaseTooltip);
+		},
+		pvTimerContent() {
+			this.$nextTick(this.updatePvTooltip);
+		},
+		vehicleClimaterTooltipContent() {
+			this.$nextTick(this.updateVehicleClimaterTooltip);
+		},
+		smartCostTooltipContent() {
+			this.$nextTick(this.updateSmartCostTooltip);
+		},
+		vehicleLimitTooltipContent() {
+			this.$nextTick(this.updateVehicleLimitTooltip);
+		},
 	},
 	computed: {
 		phaseTimerActive() {
@@ -47,6 +246,190 @@ export default {
 				this.pvRemainingInterpolated > 0 && ["enable", "disable"].includes(this.pvAction)
 			);
 		},
+		pvTimerVisible() {
+			return this.pvTimerActive;
+		},
+		pvTimerContent() {
+			if (!this.pvTimerVisible) {
+				return "";
+			}
+			const key = this.pvAction === "enable" ? "pvEnable" : "pvDisable";
+			return this.$t(`main.vehicleStatus.${key}`);
+		},
+		phaseTimerVisible() {
+			return !this.pvTimerActive && this.charging && this.phaseTimerActive;
+		},
+		phaseIconClass() {
+			return this.phaseAction === "scale1p" ? "phaseUp" : "phaseDown";
+		},
+		phaseTimerContent() {
+			if (!this.phaseTimerVisible) {
+				return "";
+			}
+			return this.$t(`main.vehicleStatus.${this.phaseAction}`);
+		},
+		vehicleLimitVisible() {
+			const limit = this.effectiveLimitSoc || 100;
+			return this.vehicleLimitSoc > 0 && this.vehicleLimitSoc < limit;
+		},
+		vehicleLimitTooltipContent() {
+			if (!this.vehicleLimitVisible) {
+				return "";
+			}
+			if (this.vehicleLimitReached) {
+				return this.$t("main.vehicleStatus.vehicleLimitReached");
+			}
+			if (this.vehicleLimitWarning) {
+				return this.$t("main.targetCharge.targetIsAboveVehicleLimit");
+			}
+			return this.$t("main.vehicleStatus.vehicleLimit");
+		},
+		minSocVisible() {
+			return this.minSoc > 0 && this.vehicleSoc < this.minSoc;
+		},
+		minSocTooltipContent() {
+			if (!this.minSocVisible) {
+				return "";
+			}
+			return this.$t("main.vehicleStatus.minCharge", {
+				soc: this.fmtPercentage(this.minSoc),
+			});
+		},
+		vehicleLimitReached() {
+			return (
+				!this.charging &&
+				this.vehicleSoc &&
+				this.vehicleLimitSoc &&
+				this.vehicleSoc >= this.vehicleLimitSoc - 1
+			);
+		},
+		vehicleLimitWarning() {
+			return this.effectivePlanSoc > this.vehicleLimitSoc;
+		},
+		vehicleLimitClass() {
+			if (this.vehicleLimitWarning) {
+				return "text-warning";
+			}
+			return "";
+		},
+		vehicleLimitIconComponent() {
+			if (this.vehicleLimitReached) {
+				return VehicleLimitReachedIcon;
+			}
+			if (this.vehicleLimitWarning) {
+				return VehicleLimitWarningIcon;
+			}
+			return VehicleLimitIcon;
+		},
+		planStartVisible() {
+			return this.planProjectedStart && !this.planActive && !this.chargingPlanDisabled;
+		},
+		planStartTooltipContent() {
+			if (!this.planStartVisible) {
+				return "";
+			}
+			const duration = this.fmtDurationToTime(new Date(this.planProjectedStart));
+			return this.$t("main.vehicleStatus.targetChargePlanned", { duration });
+		},
+		planActiveVisible() {
+			return this.planProjectedEnd && this.planActive && !this.chargingPlanDisabled;
+		},
+		planActiveClass() {
+			return this.planTimeUnreachable ? "text-warning" : "text-primary";
+		},
+		planActiveTooltipContent() {
+			if (!this.planActiveVisible) {
+				return "";
+			}
+			if (this.planTimeUnreachable) {
+				return this.$t("main.targetCharge.notReachableInTime", {
+					overrun: this.fmtDuration(this.planOverrun, true, "h"),
+				});
+			}
+			return this.$t("main.vehicleStatus.targetChargeActive", {
+				duration: this.fmtDurationToTime(new Date(this.planProjectedEnd)),
+			});
+		},
+		smartCostVisible() {
+			return !!this.smartCostLimit;
+		},
+		smartCostTooltipContent() {
+			if (!this.smartCostVisible) {
+				return "";
+			}
+			const prefix = `main.vehicleStatus.${this.smartCostPrice ? "cheap" : "clean"}`;
+			if (this.smartCostNowVisible) {
+				return this.$t(`${prefix}EnergyCharging`);
+			}
+			if (this.smartCostNextStart) {
+				return this.$t(`${prefix}EnergyNextStart`, {
+					duration: this.fmtDurationToTime(new Date(this.smartCostNextStart)),
+				});
+			}
+			return this.$t(`${prefix}EnergySet`);
+		},
+		smartCostPrice() {
+			return this.smartCostType !== CO2_TYPE;
+		},
+		smartCostNowVisible() {
+			if (this.smartCostPrice) {
+				return this.tariffGrid <= this.smartCostLimit;
+			}
+			return this.tariffCo2 <= this.smartCostLimit;
+		},
+		smartCostNow() {
+			if (this.smartCostPrice) {
+				return this.fmtPricePerKWh(this.tariffGrid, this.currency, true);
+			}
+			return this.fmtCo2Short(this.tariffCo2);
+		},
+		smartCostLimitFmt() {
+			if (this.smartCostPrice) {
+				return this.fmtPricePerKWh(this.smartCostLimit, this.currency, true);
+			}
+			return this.fmtCo2Short(this.smartCostLimit);
+		},
+		smartCostClass() {
+			if (this.smartCostDisabled) {
+				return "opacity-25";
+			}
+			if (this.smartCostActive) {
+				return "text-primary";
+			}
+			return "";
+		},
+		vehicleClimaterTooltipContent() {
+			return this.$t("main.vehicleStatus.climating");
+		},
+		chargerStatus() {
+			const t = (key, data) => {
+				if (this.heating) {
+					// check for special heating status translation
+					const name = `main.heatingStatus.${key}`;
+					if (this.$te(name, DEFAULT_LOCALE)) {
+						return this.$t(name, data);
+					}
+				}
+				return this.$t(`main.vehicleStatus.${key}`, data);
+			};
+
+			if (!this.connected) {
+				return t("disconnected");
+			}
+
+			if (this.enabled && !this.charging) {
+				if (this.vehicleLimitReached) {
+					return t("finished");
+				}
+				return t("waitForVehicle");
+			}
+
+			if (this.charging) {
+				return t("charging");
+			}
+
+			return t("connected");
+		},
 		message: function () {
 			const t = (key, data) => {
 				if (this.heating) {
@@ -68,11 +451,7 @@ export default {
 			}
 
 			// plan
-			if (
-				!this.chargingPlanDisabled &&
-				this.effectivePlanTime &&
-				!this.targetChargeDisabled
-			) {
+			if (!this.chargingPlanDisabled && this.effectivePlanTime) {
 				if (this.planActive && this.charging) {
 					return t("targetChargeActive");
 				}
@@ -137,5 +516,133 @@ export default {
 			return t("connected");
 		},
 	},
+	methods: {
+		openLoadpointSettings() {
+			this.$emit("open-loadpoint-settings");
+		},
+		openMinSocSettings() {
+			this.minSocTooltip?.hide();
+			this.$emit("open-minsoc-settings");
+		},
+		planStartClicked() {
+			this.planStartTooltip?.hide();
+			this.$emit("open-plan-modal");
+		},
+		planActiveClicked() {
+			this.planActiveTooltip?.hide();
+			this.$emit("open-plan-modal");
+		},
+		vehicleLimitClicked() {
+			if (this.vehicleLimitWarning) {
+				this.vehicleLimitTooltip?.hide();
+				this.$emit("open-plan-modal");
+			}
+		},
+		smartCostClicked() {
+			this.smartCostTooltip?.hide();
+			this.openLoadpointSettings();
+		},
+		updatePvTooltip() {
+			this.pvTooltip = this.updateTooltip(
+				this.pvTooltip,
+				this.pvTimerContent,
+				this.$refs.pvTimer
+			);
+		},
+		updatePhaseTooltip() {
+			this.phaseTooltip = this.updateTooltip(
+				this.phaseTooltip,
+				this.phaseTimerContent,
+				this.$refs.phaseTimer
+			);
+		},
+		updatePlanStartTooltip() {
+			this.planStartTooltip = this.updateTooltip(
+				this.planStartTooltip,
+				this.planStartTooltipContent,
+				this.$refs.planStart,
+				true
+			);
+		},
+		updatePlanActiveTooltip() {
+			this.planActiveTooltip = this.updateTooltip(
+				this.planActiveTooltip,
+				this.planActiveTooltipContent,
+				this.$refs.planActive,
+				true
+			);
+		},
+		updateMinSocTooltip() {
+			this.minSocTooltip = this.updateTooltip(
+				this.minSocTooltip,
+				this.minSocTooltipContent,
+				this.$refs.minSoc,
+				true
+			);
+		},
+		updateVehicleClimaterTooltip() {
+			this.vehicleClimaterTooltip = this.updateTooltip(
+				this.vehicleClimaterTooltip,
+				this.vehicleClimaterTooltipContent,
+				this.$refs.vehicleClimater
+			);
+		},
+		updateSmartCostTooltip() {
+			this.smartCostTooltip = this.updateTooltip(
+				this.smartCostTooltip,
+				this.smartCostTooltipContent,
+				this.$refs.smartCost,
+				true
+			);
+		},
+		updateVehicleLimitTooltip() {
+			this.vehicleLimitTooltip = this.updateTooltip(
+				this.vehicleLimitTooltip,
+				this.vehicleLimitTooltipContent,
+				this.$refs.vehicleLimit,
+				true
+			);
+		},
+		updateTooltip: function (instance, content, ref, hoverOnly = false) {
+			if (!content || !ref) {
+				if (instance) {
+					instance.dispose();
+				}
+				return;
+			}
+			if (!instance) {
+				const trigger = hoverOnly ? "hover" : "hover focus";
+				instance = new Tooltip(ref, { title: " ", trigger });
+			}
+			instance.setContent({ ".tooltip-inner": content });
+			return instance;
+		},
+	},
 };
 </script>
+
+<style scoped>
+.entry {
+	display: flex;
+	align-items: center;
+	flex-wrap: nowrap;
+	text-wrap: nowrap;
+	border: none;
+	color: inherit;
+	background: none;
+	padding: 0;
+	gap: 0.5rem;
+	transition:
+		color var(--evcc-transition-medium) linear,
+		opacity var(--evcc-transition-medium) linear;
+}
+.phaseUp {
+	transform: rotate(90deg);
+}
+.phaseDown {
+	transform: rotate(-90deg);
+}
+.tabular {
+	font-variant-numeric: tabular-nums;
+}
+</style>
diff --git a/assets/js/mixins/formatter.js b/assets/js/mixins/formatter.js
index cfff02a6..1c57dfdc 100644
--- a/assets/js/mixins/formatter.js
+++ b/assets/js/mixins/formatter.js
@@ -75,6 +75,10 @@ export default {
     fmtNumberToLocale(val, pad = 0) {
       return val.toLocaleString(this.$i18n?.locale).padStart(pad, "0");
     },
+    fmtDurationToTime(date) {
+      const diff = date - new Date();
+      return this.fmtDuration(diff / 1000, true, "h");
+    },
     fmtDurationNs(duration = 0, withUnit = true, minUnit = "s") {
       return this.fmtDuration(duration / 1e9, withUnit, minUnit);
     },
@@ -127,7 +131,7 @@ export default {
       const rtf = new Intl.RelativeTimeFormat(this.$i18n?.locale, { numeric: "auto" });
 
       if (this.isToday(date)) {
-        return rtf.formatToParts(0, "day")[0].value;
+        return ""; //rtf.formatToParts(0, "day")[0].value;
       }
       if (this.isTomorrow(date)) {
         return rtf.formatToParts(1, "day")[0].value;
@@ -155,7 +159,7 @@ export default {
         minute: "numeric",
       }).format(date);
 
-      return `${weekday} ${hour}`;
+      return `${weekday} ${hour}`.trim();
     },
     fmtFullDateTime: function (date, short) {
       return new Intl.DateTimeFormat(this.$i18n?.locale, {
diff --git a/cmd/demo.yaml b/cmd/demo.yaml
index bac2efb8..dcfe211e 100644
--- a/cmd/demo.yaml
+++ b/cmd/demo.yaml
@@ -208,6 +208,9 @@ vehicles:
       script: |
         "B"
     capacity: 80
+    limitsoc:
+      source: const
+      value: 90
   - name: vehicle_3
     type: template
     template: offline
diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go
index c084e3f4..70a19e1a 100644
--- a/core/keys/loadpoint.go
+++ b/core/keys/loadpoint.go
@@ -28,8 +28,9 @@ const (
 	Charging  = "charging"  // charging
 
 	// smart charging
-	SmartCostActive = "smartCostActive" // smart cost active
-	SmartCostLimit  = "smartCostLimit"  // smart cost limit
+	SmartCostActive    = "smartCostActive"    // smart cost active
+	SmartCostLimit     = "smartCostLimit"     // smart cost limit
+	SmartCostNextStart = "smartCostNextStart" // smart cost next start
 
 	// effective values
 	EffectivePriority   = "effectivePriority"   // effective priority
@@ -59,6 +60,7 @@ const (
 	PlanSoc            = "planSoc"            // charge plan soc goal
 	PlanActive         = "planActive"         // charge plan has determined current slot to be an active slot
 	PlanProjectedStart = "planProjectedStart" // charge plan start time (earliest slot)
+	PlanProjectedEnd   = "planProjectedEnd"   //charge plan ends (end of last slot)
 	PlanOverrun        = "planOverrun"        // charge plan goal not reachable in time
 
 	// remote control
diff --git a/core/loadpoint.go b/core/loadpoint.go
index f89c44b2..37dfeb2b 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -1615,8 +1615,9 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
 }
 
 // Update is the main control function. It reevaluates meters and charger state
-func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
-	lp.publish(keys.SmartCostActive, autoCharge)
+func (lp *Loadpoint) Update(sitePower float64, smartCostActive bool, smartCostNextStart time.Time, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
+	lp.publish(keys.SmartCostActive, smartCostActive)
+	lp.publish(keys.SmartCostNextStart, smartCostNextStart)
 	lp.processTasks()
 
 	// read and publish meters first- charge power and currents have already been updated by the site
@@ -1712,7 +1713,7 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt
 
 	case mode == api.ModeMinPV || mode == api.ModePV:
 		// cheap tariff
-		if autoCharge && lp.EffectivePlanTime().IsZero() {
+		if smartCostActive && lp.EffectivePlanTime().IsZero() {
 			err = lp.fastCharging()
 			lp.resetPhaseTimer()
 			lp.elapsePVTimer() // let PV mode disable immediately afterwards
diff --git a/core/loadpoint_plan.go b/core/loadpoint_plan.go
index 70cc0c0e..75fcb64b 100644
--- a/core/loadpoint_plan.go
+++ b/core/loadpoint_plan.go
@@ -88,9 +88,11 @@ func (lp *Loadpoint) plannerActive() (active bool) {
 	}()
 
 	var planStart time.Time
+	var planEnd time.Time
 	var planOverrun time.Duration
 	defer func() {
 		lp.publish(keys.PlanProjectedStart, planStart)
+		lp.publish(keys.PlanProjectedEnd, planEnd)
 		lp.publish(keys.PlanOverrun, planOverrun)
 	}()
 
@@ -129,6 +131,7 @@ func (lp *Loadpoint) plannerActive() (active bool) {
 	}
 
 	planStart = planner.Start(plan)
+	planEnd = planner.End(plan)
 	lp.log.DEBUG.Printf("plan: charge %v between %v until %v (%spower: %.0fW, avg cost: %.3f)",
 		planner.Duration(plan).Round(time.Second), planStart.Round(time.Second).Local(), planTime.Round(time.Second).Local(), overrun,
 		maxPower, planner.AverageCost(plan))
diff --git a/core/planner/helper.go b/core/planner/helper.go
index ef0be143..6c2977d1 100644
--- a/core/planner/helper.go
+++ b/core/planner/helper.go
@@ -17,6 +17,16 @@ func Start(plan api.Rates) time.Time {
 	return start
 }
 
+func End(plan api.Rates) time.Time {
+	var end time.Time
+	for _, slot := range plan {
+		if end.IsZero() || slot.End.After(end) {
+			end = slot.End
+		}
+	}
+	return end
+}
+
 // Duration returns the sum of all slot's durations
 func Duration(plan api.Rates) time.Duration {
 	var duration time.Duration
diff --git a/core/site.go b/core/site.go
index 5bde9e70..b6f86f09 100644
--- a/core/site.go
+++ b/core/site.go
@@ -36,7 +36,7 @@ const standbyPower = 10 // consider less than 10W as charger in standby
 // updater abstracts the Loadpoint implementation for testing
 type updater interface {
 	loadpoint.API
-	Update(availablePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
+	Update(availablePower float64, smartCostActive bool, smartCostNextStart time.Time, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
 }
 
 // meterMeasurement is used as slice element for publishing structured data
@@ -793,7 +793,16 @@ func (site *Site) update(lp updater) {
 	if rate, err := site.plannerRate(); err == nil {
 		smartCostActive = site.smartCostActive(lp, rate)
 	} else {
-		site.log.WARN.Println("smartCost:", err)
+		site.log.WARN.Println("smartCostActive:", err)
+	}
+
+	var smartCostNextStart time.Time
+	if !smartCostActive {
+		if rates, err := site.plannerRates(); err == nil {
+			smartCostNextStart = site.smartCostNextStart(lp, rates)
+		} else {
+			site.log.WARN.Println("smartCostNextStart:", err)
+		}
 	}
 
 	if sitePower, batteryBuffered, batteryStart, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
@@ -808,7 +817,7 @@ func (site *Site) update(lp updater) {
 		greenShareHome := site.greenShare(0, homePower)
 		greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)
 
-		lp.Update(sitePower, smartCostActive, batteryBuffered, batteryStart, greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints))
+		lp.Update(sitePower, smartCostActive, smartCostNextStart, batteryBuffered, batteryStart, greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints))
 
 		site.Health.Update()
 
diff --git a/core/site_battery.go b/core/site_battery.go
index 186275d7..3103c658 100644
--- a/core/site_battery.go
+++ b/core/site_battery.go
@@ -55,13 +55,17 @@ func (site *Site) applyBatteryMode(mode api.BatteryMode) error {
 	return nil
 }
 
-func (site *Site) plannerRate() (*api.Rate, error) {
+func (site *Site) plannerRates() (api.Rates, error) {
 	tariff := site.GetTariff(PlannerTariff)
 	if tariff == nil || tariff.Type() == api.TariffTypePriceStatic {
 		return nil, nil
 	}
 
-	rates, err := tariff.Rates()
+	return tariff.Rates()
+}
+
+func (site *Site) plannerRate() (*api.Rate, error) {
+	rates, err := site.plannerRates()
 	if err != nil {
 		return nil, err
 	}
@@ -79,6 +83,22 @@ func (site *Site) smartCostActive(lp loadpoint.API, rate *api.Rate) bool {
 	return limit != 0 && rate != nil && rate.Price <= limit
 }
 
+func (site *Site) smartCostNextStart(lp loadpoint.API, rate api.Rates) time.Time {
+	limit := lp.GetSmartCostLimit()
+	if limit == 0 || rate == nil {
+		return time.Time{}
+	}
+
+	now := time.Now()
+	for _, slot := range rate {
+		if slot.Start.After(now) && slot.Price <= limit {
+			return slot.Start
+		}
+	}
+
+	return time.Time{}
+}
+
 func (site *Site) updateBatteryMode() {
 	mode := api.BatteryNormal
 
diff --git a/histoire.config.js b/histoire.config.js
index f6ff3a51..53f1a99a 100644
--- a/histoire.config.js
+++ b/histoire.config.js
@@ -4,7 +4,7 @@ import { HstVue } from "@histoire/plugin-vue";
 export default defineConfig({
   plugins: [HstVue()],
   setupFile: "./histoire.setup.js",
-  viteNodeInlineDeps: [/!axios/],
+  viteNodeInlineDeps: [/!axios/, /shopicons/],
   routerMode: "hash",
   backgroundPresets: [
     {
diff --git a/i18n/de.toml b/i18n/de.toml
index 1e130628..71892dfc 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -397,8 +397,6 @@ selfConsumption = "Eigenverbrauch"
 
 [main.heatingStatus]
 charging = "Heize …"
-cheapEnergyCharging = "Heize mit günstiger Energie: {price} (Grenze {limit})"
-cleanEnergyCharging = "Heize mit sauberer Energie: {co2} (Grenze {limit})"
 waitForVehicle = "Bereit. Warte auf Heizung …"
 
 [main.loadpoint]
@@ -466,7 +464,7 @@ currentPlan = "Aktiver Plan"
 descriptionEnergy = "Bis wann sollen {targetEnergy} ins Fahrzeug geladen sein?"
 descriptionSoc = "Wann soll das Fahrzeug auf {targetSoc} geladen sein?"
 inactiveLabel = "Zielzeit"
-notReachableInTime = "Zielzeit nicht erreichbar. Voraussichtliches Ende: {endTime}."
+notReachableInTime = "Zielzeit wird {overrun} später erreicht."
 onlyInPvMode = "Ladeplanung ist nur im PV-Modus aktiv."
 planDuration = "Ladedauer"
 planPeriodLabel = "Zeitraum"
@@ -477,7 +475,7 @@ priceLimit = "Preisgrenze von {price}"
 remove = "Entfernen"
 setTargetTime = "keine"
 targetIsAboveLimit = "Das eingestellte Ladelimit von {limit} wird in diesem Zeitraum ignoriert."
-targetIsAboveVehicleLimit = "Erhöhe das Fahrzeuglimit ({limit}), um das Ladeziel zu erreichen."
+targetIsAboveVehicleLimit = "Fahrzeuglimit ist kleiner als das Ladeziel."
 targetIsInThePast = "Wähle einen Zeitpunkt in der Zukunft, Marty."
 targetIsTooFarInTheFuture = "Wir passen den Plan an, sobald wir mehr über die Zukunft wissen."
 title = "Zielzeit"
@@ -521,21 +519,27 @@ vehicleLimit = "Fahrzeuglimit: {soc}"
 
 [main.vehicleStatus]
 charging = "Ladevorgang aktiv …"
-cheapEnergyCharging = "Lade günstige Energie: {price} (Grenze {limit})"
-cleanEnergyCharging = "Lade saubere Energie: {co2} (Grenze {limit})"
+cheapEnergyCharging = "Günstige Energie verfügbar."
+cheapEnergyNextStart = "Günstige Energie in {duration}."
+cheapEnergySet = "Preislimit gesetzt."
+cleanEnergyCharging = "Saubere Energie verfügbar."
+cleanEnergyNextStart = "Saubere Energie in {duration}."
+cleanEnergySet = "CO₂-Limit gesetzt."
 climating = "Vorklimatisierung erkannt."
 connected = "Verbunden."
 disconnected = "Nicht verbunden."
+finished = "Abgeschlossen."
 minCharge = "Mindestladung bis {soc}."
-pvDisable = "Zu wenig Überschuss. Pausiere in {remaining} …"
-pvEnable = "Überschuss verfügbar. Starte in {remaining} …"
-scale1p = "Reduziere auf einphasig in {remaining} …"
-scale3p = "Erhöhe auf dreiphasig in {remaining} …"
-targetChargeActive = "Ladeplan aktiv …"
-targetChargePlanned = "Ladeplan startet {time} Uhr."
+pvDisable = "Zu wenig Überschuss. Ladung wird gleich pausiert."
+pvEnable = "Überschuss verfügbar. Ladung wird gleich fortgesetzt."
+scale1p = "Reduziere gleich auf einphasiges Laden."
+scale3p = "Erhöhe gleich auf dreiphasiges Laden."
+targetChargeActive = "Ladeplan aktiv. Geplantes Ende in {duration}."
+targetChargePlanned = "Ladeplan startet in {duration}."
 targetChargeWaitForVehicle = "Ladeplan bereit. Warte auf Fahrzeug …"
 unknown = ""
-vehicleLimitReached = "Fahrzeuglimit {soc} erreicht."
+vehicleLimit = "Fahrzeuglimit."
+vehicleLimitReached = "Fahrzeuglimit erreicht."
 waitForVehicle = "Ladebereit. Warte auf Fahrzeug …"
 
 [notifications]
diff --git a/i18n/en.toml b/i18n/en.toml
index dcdf1fc4..fb8893ce 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -395,8 +395,6 @@ selfConsumption = "Self-consumption"
 
 [main.heatingStatus]
 charging = "Heating…"
-cheapEnergyCharging = "Heating with cheap energy: {price} (limit {limit})"
-cleanEnergyCharging = "Heating with clean energy: {co2} (limit {limit})"
 waitForVehicle = "Ready. Waiting for heater…"
 
 [main.loadpoint]
@@ -464,7 +462,7 @@ currentPlan = "Active plan"
 descriptionEnergy = "Until when should {targetEnergy} be loaded into the vehicle?"
 descriptionSoc = "When should the vehicle be charged to {targetSoc}?"
 inactiveLabel = "Target time"
-notReachableInTime = "Goal not reachable in time. Estimated finish: {endTime}."
+notReachableInTime = "Goal will be reached {overrun} later."
 onlyInPvMode = "Charging plan only works in solar mode."
 planDuration = "Charging time"
 planPeriodLabel = "Period"
@@ -475,7 +473,7 @@ priceLimit = "price limit of {price}"
 remove = "Remove"
 setTargetTime = "none"
 targetIsAboveLimit = "The configured charging limit of {limit} will be ignored during this period."
-targetIsAboveVehicleLimit = "Increase vehicle limit ({limit}) to reach the charging goal."
+targetIsAboveVehicleLimit = "Vehicle limit is below charging goal."
 targetIsInThePast = "Pick a time in the future, Marty."
 targetIsTooFarInTheFuture = "We will adjust the plan as soon as we know more about the future."
 title = "Target Time"
@@ -519,21 +517,27 @@ vehicleLimit = "Vehicle limit: {soc}"
 
 [main.vehicleStatus]
 charging = "Charging…"
-cheapEnergyCharging = "Charging cheap energy: {price} (limit {limit})"
-cleanEnergyCharging = "Charging clean energy: {co2} (limit {limit})"
+cheapEnergyCharging = "Cheap energy available."
+cheapEnergyNextStart = "Cheap energy in {duration}."
+cheapEnergySet = "Price limit set."
+cleanEnergyCharging = "Clean energy available."
+cleanEnergyNextStart = "Clean energy in {duration}."
+cleanEnergySet = "CO₂ limit set."
 climating = "Pre-conditioning detected."
 connected = "Connected."
 disconnected = "Disconnected."
+finished = "Finished."
 minCharge = "Minimum charging to {soc}."
-pvDisable = "Not enough surplus. Pausing in {remaining}…"
-pvEnable = "Surplus available. Starting in {remaining}…"
-scale1p = "Reducing to 1-phase charging in {remaining}…"
-scale3p = "Increasing to 3-phase charging in {remaining}…"
-targetChargeActive = "Charging plan active…"
-targetChargePlanned = "Charging plan starts at {time}."
+pvDisable = "Not enough surplus. Pausing soon."
+pvEnable = "Surplus available. Starting soon."
+scale1p = "Reducing to 1-phase charging soon."
+scale3p = "Increasing to 3-phase charging soon."
+targetChargeActive = "Charging plan active. Estimated finish in {duration}."
+targetChargePlanned = "Charging plan starts in {duration}."
 targetChargeWaitForVehicle = "Charging plan ready. Waiting for vehicle…"
 unknown = ""
-vehicleLimitReached = "Vehicle limit {soc} reached."
+vehicleLimit = "Vehicle limit."
+vehicleLimitReached = "Vehicle limit reached."
 waitForVehicle = "Ready. Waiting for vehicle…"
 
 [notifications]


Test patch:
diff --git a/assets/js/components/VehicleStatus.test.js b/assets/js/components/VehicleStatus.test.js
index 29af5db7..fa56a8f9 100644
--- a/assets/js/components/VehicleStatus.test.js
+++ b/assets/js/components/VehicleStatus.test.js
@@ -6,70 +6,109 @@ const serializeData = (data) => (data ? `:${JSON.stringify(data)}` : "");
 config.global.mocks["$t"] = (key, data) => `${key}${serializeData(data)}`;
 config.global.mocks["$i18n"] = { locale: "de-DE" };
 
-const expectStatus = (props, messageKey, data) => {
+const allEntries = {
+  pvtimer: false,
+  phasetimer: false,
+  solar: false,
+  climater: false,
+  minsoc: false,
+  limit: false,
+  smartcost: false,
+  planactive: false,
+  planstart: false,
+};
+
+const expectEntries = (props, entries) => {
+  const expectedEntries = { ...allEntries, ...entries };
+
   const wrapper = mount(VehicleStatus, { props });
-  expect(wrapper.find("div").text()).eq(`main.vehicleStatus.${messageKey}${serializeData(data)}`);
+
+  Object.entries(expectedEntries).forEach(([key, value]) => {
+    const selector = `[data-testid=vehicle-status-${key}]`;
+    if (typeof value === "boolean") {
+      expect(wrapper.find(selector).exists(), selector).eq(value);
+    } else {
+      expect(wrapper.find(selector).exists(), selector).eq(true);
+      expect(wrapper.find(selector).text(), selector).eq(value);
+    }
+  });
 };
 
 describe("basics", () => {
   test("no vehicle is connected", () => {
-    expectStatus({ connected: false }, "disconnected");
+    expectEntries({ connected: false }, { charger: "main.vehicleStatus.disconnected" });
   });
   test("vehicle is connected", () => {
-    expectStatus({ connected: true }, "connected");
+    expectEntries({ connected: true }, { charger: "main.vehicleStatus.connected" });
   });
   test("show waiting for vehicle if charger is enabled but not charging", () => {
-    expectStatus({ enabled: true, connected: true }, "waitForVehicle");
+    expectEntries(
+      { enabled: true, connected: true },
+      { charger: "main.vehicleStatus.waitForVehicle" }
+    );
   });
   test("vehicle is charging", () => {
-    expectStatus({ connected: true, charging: true }, "charging");
+    expectEntries({ connected: true, charging: true }, { charger: "main.vehicleStatus.charging" });
   });
 });
 
 describe("min charge", () => {
   test("active when vehicle soc is below", () => {
-    expectStatus({ connected: true, minSoc: 20, vehicleSoc: 10 }, "minCharge", { soc: "20 %" });
+    expectEntries(
+      { connected: true, minSoc: 20, vehicleSoc: 10 },
+      { charger: "main.vehicleStatus.connected", minsoc: "20 %" }
+    );
   });
   test("not active when vehicle soc is above", () => {
-    expectStatus({ connected: true, minSoc: 20, vehicleSoc: 21 }, "connected");
+    expectEntries(
+      { connected: true, minSoc: 20, vehicleSoc: 21 },
+      { charger: "main.vehicleStatus.connected", minsoc: false }
+    );
   });
   test("not active when vehicle soc is equal", () => {
-    expectStatus({ connected: true, minSoc: 20, vehicleSoc: 20 }, "connected");
+    expectEntries(
+      { connected: true, minSoc: 20, vehicleSoc: 20 },
+      { charger: "main.vehicleStatus.connected", minsoc: false }
+    );
   });
   test("not active when limit is 0", () => {
-    expectStatus({ connected: true, minSoc: 0, vehicleSoc: 10 }, "connected");
+    expectEntries(
+      { connected: true, minSoc: 0, vehicleSoc: 10 },
+      { charger: "main.vehicleStatus.connected", minsoc: false }
+    );
   });
 });
 
 describe("plan", () => {
   const effectivePlanTime = "2020-03-16T06:00:00Z";
   const planProjectedStart = "2020-03-16T02:00:00Z";
+  const planProjectedEnd = "2020-03-16T05:00:00Z";
   test("charging if target time is set, status is charging but planned slot is not active", () => {
-    expectStatus({ effectivePlanTime, charging: true, connected: true }, "charging");
+    expectEntries(
+      { effectivePlanTime, charging: true, connected: true },
+      { charger: "main.vehicleStatus.charging" }
+    );
   });
   test("active if target time is set, status is charging and planned slot is active", () => {
-    expectStatus(
-      { effectivePlanTime, planActive: true, charging: true, connected: true },
-      "targetChargeActive"
+    expectEntries(
+      { planProjectedEnd, planActive: true, charging: true, connected: true },
+      { charger: "main.vehicleStatus.charging", planactive: "Mo 06:00" }
     );
   });
   test("waiting for vehicle if a target time is set, the charger is enabled but not charging", () => {
-    expectStatus(
-      { effectivePlanTime, planActive: true, enabled: true, connected: true },
-      "targetChargeWaitForVehicle"
+    expectEntries(
+      { planProjectedEnd, planActive: true, enabled: true, connected: true },
+      { charger: "main.vehicleStatus.waitForVehicle", planactive: "Mo 06:00" }
     );
   });
   test("show projected start if not enabled yet", () => {
-    expectStatus(
+    expectEntries(
       { effectivePlanTime, planProjectedStart, connected: true },
-      "targetChargePlanned",
-      {
-        time: "Mo 03:00",
-      }
+      { charger: "main.vehicleStatus.connected", planstart: "Mo 03:00" }
     );
   });
   test("dont show plan status if plan is disabled (e.g. off, fast mode)", () => {
-    expectStatus(
+    expectEntries(
       {
         effectivePlanTime,
         planActive: true,
@@ -77,131 +116,142 @@ describe("plan", () => {
         connected: true,
         chargingPlanDisabled: true,
       },
-      "charging"
+      { charger: "main.vehicleStatus.charging" }
     );
-    expectStatus(
+    expectEntries(
       {
         effectivePlanTime,
         planActive: true,
+        charging: false,
         enabled: true,
         connected: true,
         chargingPlanDisabled: true,
       },
-      "waitForVehicle"
+      { charger: "main.vehicleStatus.waitForVehicle" }
     );
-    expectStatus(
+    expectEntries(
       { effectivePlanTime, planProjectedStart, connected: true, chargingPlanDisabled: true },
-      "connected"
+      { charger: "main.vehicleStatus.connected" }
     );
   });
 });
 
 describe("climating", () => {
   test("show climating status", () => {
-    expectStatus(
+    expectEntries(
       { connected: true, enabled: true, vehicleClimaterActive: true, charging: true },
-      "climating"
+      { charger: "main.vehicleStatus.charging", climater: true }
     );
-    expectStatus(
+    expectEntries(
       { connected: true, enabled: true, vehicleClimaterActive: true, charging: false },
-      "climating"
+      { charger: "main.vehicleStatus.waitForVehicle", climater: true }
     );
   });
   test("only show climating if enabled", () => {
-    expectStatus({ connected: true, enabled: false, vehicleClimaterActive: true }, "connected");
+    expectEntries(
+      { connected: true, enabled: false, vehicleClimaterActive: true, charging: false },
+      { charger: "main.vehicleStatus.connected", climater: true }
+    );
   });
 });
 
 describe("timer", () => {
   test("show pv enable timer if not enabled yet and timer exists", () => {
-    expectStatus(
+    expectEntries(
       {
         pvAction: "enable",
         connected: true,
         pvRemainingInterpolated: 90,
       },
-      "pvEnable",
-      { remaining: "1:30\u202Fm" }
+      { charger: "main.vehicleStatus.connected", pvtimer: "1:30\u202Fm" }
     );
   });
   test("don't show pv enable timer if value is zero", () => {
-    expectStatus(
+    expectEntries(
       {
         pvAction: "enable",
         connected: true,
         pvRemainingInterpolated: 0,
       },
-      "connected"
+      { charger: "main.vehicleStatus.connected" }
     );
   });
   test("show pv disable timer if charging and timer exists", () => {
-    expectStatus(
+    expectEntries(
       {
         pvAction: "disable",
         connected: true,
         charging: true,
         pvRemainingInterpolated: 90,
       },
-      "pvDisable",
-      { remaining: "1:30\u202Fm" }
+      { charger: "main.vehicleStatus.charging", pvtimer: "1:30\u202Fm" }
     );
   });
   test("show phase enable timer if it exists", () => {
-    expectStatus(
+    expectEntries(
       {
         phaseAction: "scale1p",
         connected: true,
         charging: true,
-        phaseRemainingInterpolated: 90,
+        phaseRemainingInterpolated: 91,
       },
-      "scale1p",
-      { remaining: "1:30\u202Fm" }
+      { charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
     );
   });
   test("show phase disable timer if it exists", () => {
-    expectStatus(
+    expectEntries(
       {
         phaseAction: "scale3p",
         connected: true,
         charging: true,
-        phaseRemainingInterpolated: 90,
+        phaseRemainingInterpolated: 91,
       },
-      "scale3p",
-      { remaining: "1:30\u202Fm" }
+      { charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
     );
   });
 });
 
 describe("vehicle target soc", () => {
   test("show target reached if charger enabled but soc has reached vehicle limit", () => {
-    expectStatus(
+    expectEntries(
       {
         connected: true,
         enabled: true,
         vehicleLimitSoc: 70,
         vehicleSoc: 70,
       },
-      "vehicleLimitReached",
-      { soc: "70 %" }
+      { charger: "main.vehicleStatus.finished", limit: "70 %" }
     );
   });
   test("show reached message even if vehicle is slightly below its limit", () => {
-    expectStatus(
+    expectEntries(
       {
         connected: true,
         enabled: true,
         vehicleLimitSoc: 70,
         vehicleSoc: 69,
       },
-      "vehicleLimitReached",
-      { soc: "70 %" }
+      { charger: "main.vehicleStatus.finished", limit: "70 %" }
     );
   });
 });
 
 describe("smart grid charging", () => {
   test("show clean energy message", () => {
-    expectStatus(
+    expectEntries(
+      {
+        connected: true,
+        enabled: true,
+        charging: true,
+        tariffCo2: 600,
+        smartCostLimit: 500,
+        smartCostType: "co2",
+      },
+      { charger: "main.vehicleStatus.charging", smartcost: "≤ 500 g" }
+    );
+  });
+  test("show clean energy message if active", () => {
+    expectEntries(
       {
         connected: true,
         enabled: true,
@@ -211,11 +261,24 @@ describe("smart grid charging", () => {
         smartCostType: "co2",
         smartCostActive: true,
       },
-      `cleanEnergyCharging:{"co2":"400 g","limit":"500 g"}`
+      { charger: "main.vehicleStatus.charging", smartcost: "400 g ≤ 500 g" }
     );
   });
   test("show cheap energy message", () => {
-    expectStatus(
+    expectEntries(
+      {
+        connected: true,
+        enabled: true,
+        charging: true,
+        tariffGrid: 0.3,
+        smartCostLimit: 0.29,
+        currency: "CHF",
+      },
+      { charger: "main.vehicleStatus.charging", smartcost: "≤ 29,0 rp" }
+    );
+  });
+  test("show cheap energy message if active", () => {
+    expectEntries(
       {
         connected: true,
         enabled: true,
@@ -225,7 +288,7 @@ describe("smart grid charging", () => {
         currency: "CHF",
         smartCostActive: true,
       },
-      `cheapEnergyCharging:{"price":"28,0 rp","limit":"29,0 rp"}`
+      { charger: "main.vehicleStatus.charging", smartcost: "28,0 rp ≤ 29,0 rp" }
     );
   });
 });
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 92ecbaf1..2c74d474 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -182,7 +182,7 @@ func TestUpdatePowerZero(t *testing.T) {
 		}
 
 		lp.mode = tc.mode
-		lp.Update(0, false, false, false, 0, nil, nil) // false,sitePower false,0
+		lp.Update(0, false, time.Time{}, false, false, 0, nil, nil) // false,sitePower false,0
 
 		ctrl.Finish()
 	}
@@ -428,7 +428,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 	charger.EXPECT().Status().Return(api.StatusC, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
-	lp.Update(500, false, false, false, 0, nil, nil)
+	lp.Update(500, false, time.Time{}, false, false, 0, nil, nil)
 	ctrl.Finish()
 
 	t.Log("charging above target - soc deactivates charger")
@@ -437,7 +437,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 	charger.EXPECT().Status().Return(api.StatusC, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Enable(false).Return(nil)
-	lp.Update(500, false, false, false, 0, nil, nil)
+	lp.Update(500, false, time.Time{}, false, false, 0, nil, nil)
 	ctrl.Finish()
 
 	t.Log("deactivated charger changes status to B")
@@ -445,14 +445,14 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 	vehicle.EXPECT().Soc().Return(95.0, nil)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
-	lp.Update(-5000, false, false, false, 0, nil, nil)
+	lp.Update(-5000, false, time.Time{}, false, false, 0, nil, nil)
 	ctrl.Finish()
 
 	t.Log("soc has fallen below target - soc update prevented by timer")
 	clock.Add(5 * time.Minute)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
-	lp.Update(-5000, false, false, false, 0, nil, nil)
+	lp.Update(-5000, false, time.Time{}, false, false, 0, nil, nil)
 	ctrl.Finish()
 
 	t.Log("soc has fallen below target - soc update timer expired")
@@ -461,7 +461,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 	charger.EXPECT().Status().Return(api.StatusB, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Enable(true).Return(nil)
-	lp.Update(-5000, false, false, false, 0, nil, nil)
+	lp.Update(-5000, false, time.Time{}, false, false, 0, nil, nil)
 	ctrl.Finish()
 }
 
@@ -496,14 +496,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
 	charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
-	lp.Update(500, false, false, false, 0, nil, nil)
+	lp.Update(500, false, time.Time{}, false, false, 0, nil, nil)
 
 	t.Log("switch off when disconnected")
 	clock.Add(5 * time.Minute)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusA, nil)
 	charger.EXPECT().Enable(false).Return(nil)
-	lp.Update(-3000, false, false, false, 0, nil, nil)
+	lp.Update(-3000, false, time.Time{}, false, false, 0, nil, nil)
 
 	if mode := lp.GetMode(); mode != api.ModeOff {
 		t.Error("unexpected mode", mode)
@@ -566,14 +566,14 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(0.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false, false, false, 0, nil, nil)
+	lp.Update(-1, false, time.Time{}, false, false, 0, nil, nil)
 
 	t.Log("at 1:00h charging at 5 kWh")
 	clock.Add(time.Hour)
 	rater.EXPECT().ChargedEnergy().Return(5.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false, false, false, 0, nil, nil)
+	lp.Update(-1, false, time.Time{}, false, false, 0, nil, nil)
 	expectCache("chargedEnergy", 5000.0)
 
 	t.Log("at 1:00h stop charging at 5 kWh")
@@ -581,7 +581,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(5.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
-	lp.Update(-1, false, false, false, 0, nil, nil)
+	lp.Update(-1, false, time.Time{}, false, false, 0, nil, nil)
 	expectCache("chargedEnergy", 5000.0)
 
 	t.Log("at 1:00h restart charging at 5 kWh")
@@ -589,7 +589,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(5.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false, false, false, 0, nil, nil)
+	lp.Update(-1, false, time.Time{}, false, false, 0, nil, nil)
 	expectCache("chargedEnergy", 5000.0)
 
 	t.Log("at 1:30h continue charging at 7.5 kWh")
@@ -597,7 +597,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(7.5, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusC, nil)
-	lp.Update(-1, false, false, false, 0, nil, nil)
+	lp.Update(-1, false, time.Time{}, false, false, 0, nil, nil)
 	expectCache("chargedEnergy", 7500.0)
 
 	t.Log("at 2:00h stop charging at 10 kWh")
@@ -605,7 +605,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	rater.EXPECT().ChargedEnergy().Return(10.0, nil)
 	charger.EXPECT().Enabled().Return(lp.enabled, nil)
 	charger.EXPECT().Status().Return(api.StatusB, nil)
-	lp.Update(-1, false, false, false, 0, nil, nil)
+	lp.Update(-1, false, time.Time{}, false, false, 0, nil, nil)
 	expectCache("chargedEnergy", 10000.0)
 
 	ctrl.Finish()
diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go
index 927293de..3ac7320b 100644
--- a/core/loadpoint_vehicle_test.go
+++ b/core/loadpoint_vehicle_test.go
@@ -270,7 +270,7 @@ func TestReconnectVehicle(t *testing.T) {
 			// vehicle not updated yet
 			vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)
 
-			lp.Update(0, false, false, false, 0, nil, nil)
+			lp.Update(0, false, time.Time{}, false, false, 0, nil, nil)
 			ctrl.Finish()
 
 			// detection started
@@ -284,7 +284,7 @@ func TestReconnectVehicle(t *testing.T) {
 			// vehicle not updated yet
 			vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil)
 
-			lp.Update(0, false, false, false, 0, nil, nil)
+			lp.Update(0, false, time.Time{}, false, false, 0, nil, nil)
 			ctrl.Finish()
 
 			// vehicle detected
diff --git a/tests/plan.spec.js b/tests/plan.spec.js
index 92020e30..c7e34ff5 100644
--- a/tests/plan.spec.js
+++ b/tests/plan.spec.js
@@ -62,7 +62,8 @@ test.describe("basic functionality", async () => {
       "tomorrow 9:30 AM80%"
     );
 
-    await expect(lp1.getByTestId("vehicle-status")).toContainText("Charging plan starts at");
+    await expect(lp1.getByTestId("vehicle-status-charger")).toHaveText("Connected.");
+    await expect(lp1.getByTestId("vehicle-status-planstart")).toHaveText(/tomorrow .* AM/);
     await expect(lp1.getByTestId("plan-marker")).toBeVisible();
     await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
       "tomorrow 9:30 AM80%"
@@ -277,9 +278,8 @@ test.describe("warnings", async () => {
 
     await page.getByTestId("plan-active").click();
 
-    await expect(page.getByTestId("plan-warnings")).toContainText(
-      "Goal not reachable in time. Estimated finish"
-    );
+    // match this text but with fuzzy date "getByText('Goal will be reached 52:10 h')"
+    await expect(page.getByTestId("plan-warnings")).toHaveText(/Goal will be reached .* later/);
   });
   test("time in the past", async ({ page }) => {
     await page.goto("/");
diff --git a/tests/smart-cost.spec.js b/tests/smart-cost.spec.js
index a0f3ae58..ec6a8e10 100644
--- a/tests/smart-cost.spec.js
+++ b/tests/smart-cost.spec.js
@@ -25,7 +25,7 @@ test.beforeEach(async ({ page }) => {
 test.describe("smart cost limit", async () => {
   test("no limit, normal charging", async ({ page }) => {
     await page.goto("/");
-    await expect(page.getByTestId("vehicle-status")).toHaveText("Charging…");
+    await expect(page.getByTestId("vehicle-status-charger")).toHaveText("Charging…");
   });
   test("price below limit", async ({ page }) => {
     await page.goto("/");
@@ -37,8 +37,8 @@ test.describe("smart cost limit", async () => {
       .selectOption("≤ 40.0 ct/kWh");
     await page.getByTestId("loadpoint-settings-modal").getByLabel("Close").click();
     await expect(page.getByTestId("loadpoint-settings-modal")).not.toBeVisible();
-    await expect(page.getByTestId("vehicle-status")).toContainText("Charging cheap energy");
-    await expect(page.getByTestId("vehicle-status")).toContainText("(limit 40.0 ct)");
+    await expect(page.getByTestId("vehicle-status-charger")).toHaveText("Charging…");
+    await expect(page.getByTestId("vehicle-status-smartcost")).toHaveText("40.0 ct ≤ 40.0 ct");
   });
   test("price above limit", async ({ page }) => {
     await page.goto("/");
@@ -50,6 +50,7 @@ test.describe("smart cost limit", async () => {
       .selectOption("≤ 10.0 ct/kWh");
     await page.getByTestId("loadpoint-settings-modal").getByLabel("Close").click();
     await expect(page.getByTestId("loadpoint-settings-modal")).not.toBeVisible();
-    await expect(page.getByTestId("vehicle-status")).toHaveText("Charging…");
+    await expect(page.getByTestId("vehicle-status-charger")).toHaveText("Charging…");
+    await expect(page.getByTestId("vehicle-status-smartcost")).toHaveText("≤ 10.0 ct");
   });
 });
diff --git a/tests/vehicle-settings.spec.js b/tests/vehicle-settings.spec.js
index 42940af6..86b8199f 100644
--- a/tests/vehicle-settings.spec.js
+++ b/tests/vehicle-settings.spec.js
@@ -44,7 +44,7 @@ test.describe("minSoc", async () => {
     await expect(page.getByText("charged to 20% in solar mode")).toBeVisible();
   });
 
-  test("show minsoc instead of plan when minsoc is active", async ({ page }) => {
+  test("show minsoc indicator when minsoc is active", async ({ page }) => {
     await page.goto("/");
 
     await expect(page.getByTestId("charging-plan")).toContainText("Plan");
@@ -53,14 +53,14 @@ test.describe("minSoc", async () => {
     await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("50%");
     await page.getByRole("button", { name: "Close" }).click();
 
-    await expect(page.getByTestId("charging-plan")).toContainText("Min charge");
-    await expect(page.getByTestId("vehicle-status")).toHaveText("Minimum charging to 50%.");
-    await page.getByTestId("charging-plan").getByRole("button", { name: "50%" }).click();
+    await expect(page.getByTestId("vehicle-status-minsoc")).toBeVisible();
+    await expect(page.getByTestId("vehicle-status-minsoc")).toHaveText("50%");
 
+    await page.getByTestId("vehicle-status-minsoc").click();
     await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("---");
     await page.getByRole("button", { name: "Close" }).click();
-    await expect(page.getByTestId("charging-plan")).toContainText("Plan");
-    await expect(page.getByTestId("charging-plan").getByRole("button")).toHaveText("none");
+
+    await expect(page.getByTestId("vehicle-status-minsoc")).not.toBeVisible();
   });
 });