UI: move vehicle settings from charging plan modal to own modal (#31775)

This commit is contained in:
Michael Geers 2026-07-19 11:39:33 +02:00 • committed by GitHub
parent f673c6d0c8
commit 2a86cc44b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 690 additions and 796 deletions

View file

@ -745,6 +745,14 @@ html.app .modal-dialog {
outline: revert;
}
/* selects match :focus-visible on click, CustomSelect sets .focus-ring-visible instead */
.keyboard-focus-ring:focus-visible,
.keyboard-focus-ring.focus-ring-visible {
outline: var(--bs-focus-ring-width) solid var(--bs-focus-ring-color);
outline-offset: 2px;
border-radius: var(--bs-border-radius);
}
.round-box {
border-radius: 1rem;
color: var(--evcc-default-text);

View file

@ -28,6 +28,7 @@
<MoreItem
:active="moreActive"
:vehicles="vehicles"
:auth-providers="authProviders"
:sponsor="sponsor"
:fatal="fatal"
@ -50,7 +51,15 @@ import BatteryIcon from "../Energyflow/BatteryIcon.vue";
import Item from "./Item.vue";
import MoreItem from "./MoreItem.vue";
import { defineComponent, type PropType } from "vue";
import type { FatalError, Forecast, Sponsor, EvOpt, AuthProviders, Battery } from "@/types/evcc";
import type {
FatalError,
Forecast,
Sponsor,
EvOpt,
AuthProviders,
Battery,
Vehicle,
} from "@/types/evcc";
export default defineComponent({
name: "BottomTabBar",
@ -66,6 +75,7 @@ export default defineComponent({
batteryGridChargeActive: Boolean,
batteryMode: { type: String as PropType<string> },
forecast: { type: Object as PropType<Forecast> },
vehicles: { type: Object as PropType<Record<string, Vehicle>>, default: () => ({}) },
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
sponsor: { type: Object as PropType<Sponsor>, default: () => ({}) },
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },

View file

@ -11,6 +11,7 @@
<template #menu>
<MoreMenu
:open="open"
:vehicles="vehicles"
:auth-providers="authProviders"
:sponsor="sponsor"
:fatal="fatal"
@ -34,13 +35,14 @@ import MoreMenu from "./MoreMenu.vue";
import { isUserConfigError } from "@/utils/fatal";
import { isNewVersionAvailable, isNewVersionUnacknowledged } from "@/utils/version";
import settings from "@/settings";
import type { FatalError, Sponsor, EvOpt, AuthProviders } from "@/types/evcc";
import type { FatalError, Sponsor, EvOpt, AuthProviders, Vehicle } from "@/types/evcc";
export default defineComponent({
name: "MoreItem",
components: { Item, MoreIcon, MoreMenu },
props: {
active: Boolean,
vehicles: { type: Object as PropType<Record<string, Vehicle>>, default: () => ({}) },
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
sponsor: { type: Object as PropType<Sponsor>, default: () => ({}) },
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },

View file

@ -57,6 +57,14 @@
<button type="button" class="dropdown-item" @click="openSettingsModal">
{{ $t("settings.title") }}
</button>
<button
v-if="hasVehicles"
type="button"
class="dropdown-item"
@click="openVehicleSettingsModal"
>
{{ $t("main.vehicleSettings.menu") }}
</button>
<router-link class="dropdown-item" to="/config" active-class="active">
<span v-if="showConfigBadge" class="circle-badge me-1" :class="badgeClass"></span>
{{ $t("config.main.title") }}
@ -88,12 +96,13 @@ import {
import settings from "@/settings";
import { isUserConfigError } from "@/utils/fatal";
import { defineComponent, type PropType } from "vue";
import type { FatalError, Sponsor, EvOpt, AuthProviders } from "@/types/evcc";
import type { FatalError, Sponsor, EvOpt, AuthProviders, Vehicle } from "@/types/evcc";
export default defineComponent({
name: "MoreMenu",
props: {
open: { type: Boolean, default: false },
vehicles: { type: Object as PropType<Record<string, Vehicle>>, default: () => ({}) },
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
sponsor: { type: Object as PropType<Sponsor>, default: () => ({}) },
fatal: { type: Array as PropType<FatalError[]>, default: () => [] },
@ -155,6 +164,9 @@ export default defineComponent({
showLogout() {
return !this.authDisabled && isLoggedIn();
},
hasVehicles() {
return Object.keys(this.vehicles).length > 0;
},
},
mounted() {
this.onClickOutside = (e: MouseEvent) => {
@ -191,6 +203,12 @@ export default defineComponent({
);
modal.show();
},
openVehicleSettingsModal() {
const modal = Modal.getOrCreateInstance(
document.getElementById("vehicleSettingsModal") as HTMLElement
);
modal.show();
},
openNativeSettings() {
sendToApp({ type: "settings" });
},

View file

@ -1,162 +0,0 @@
<template>
<div class="mt-4 container">
<div class="row mb-4">
<label :for="formId('mode')" class="col-12 col-md-4 col-lg-3 col-form-label">
{{ $t("config.loadpoint.defaultModeLabel") }}
</label>
<div class="col-12 col-md-8 col-lg-6">
<select
:id="formId('mode')"
v-model="selectedMode"
class="form-select"
@change="changeMode"
>
<option v-for="opt in modeOptions" :key="opt.value" :value="opt.value">
{{ opt.name }}
</option>
</select>
<small class="form-text text-muted">{{ modeHelp }}</small>
</div>
</div>
<template v-if="socSettingsVisible">
<div class="row mb-4">
<label :for="formId('minsoc')" class="col-12 col-md-4 col-lg-3 col-form-label">
{{ $t("main.loadpointSettings.minSoc.label") }}
</label>
<div class="col-12 col-md-8 col-lg-6">
<select
:id="formId('minsoc')"
v-model.number="selectedMinSoc"
class="form-select"
@change="changeMinSoc"
>
<option v-for="soc in minSocOptions" :key="soc.value" :value="soc.value">
{{ soc.name }}
</option>
</select>
<small class="form-text text-muted">
{{
$t("main.loadpointSettings.minSoc.description", [
selectedMinSoc ? fmtPercentage(selectedMinSoc) : "x",
])
}}
</small>
</div>
</div>
<div class="row mb-4">
<label :for="formId('limitsoc')" class="col-12 col-md-4 col-lg-3 col-form-label">
{{ $t("main.loadpointSettings.limitSoc.label") }}
</label>
<div class="col-12 col-md-8 col-lg-6">
<select
:id="formId('limitsoc')"
v-model.number="selectedLimitSoc"
class="form-select"
@change="changeLimitSoc"
>
<option v-for="soc in limitSocOptions" :key="soc.value" :value="soc.value">
{{ soc.name }}
</option>
</select>
<small class="form-text text-muted">
{{ $t("main.loadpointSettings.limitSoc.description") }}
</small>
</div>
</div>
</template>
</div>
</template>
<script lang="ts">
import { distanceUnit } from "@/units";
import formatter from "@/mixins/formatter";
import { CHARGE_MODE, type SelectOption } from "@/types/evcc";
import { defineComponent } from "vue";
const { OFF, PV, MINPV, NOW } = CHARGE_MODE;
export default defineComponent({
name: "ChargingPlanArrival",
mixins: [formatter],
props: {
id: [String, Number],
minSoc: { type: Number, default: 0 },
limitSoc: { type: Number, default: 0 },
mode: { type: String, default: "" },
vehicleName: String,
vehicleNotReachable: Boolean,
socBasedCharging: Boolean,
rangePerSoc: Number,
},
emits: ["minsoc-updated", "limitsoc-updated", "mode-updated"],
data() {
return {
selectedMinSoc: this.minSoc,
selectedLimitSoc: this.limitSoc,
selectedMode: this.mode,
};
},
computed: {
modeOptions(): SelectOption<string>[] {
return [
{ value: "", name: this.$t("config.loadpoint.defaultModeKeep") },
...[OFF, PV, MINPV, NOW].map((mode) => ({
value: mode,
name: this.$t(`main.mode.${mode}`),
})),
];
},
modeHelp(): string {
return this.selectedMode === ""
? this.$t("config.loadpoint.defaultModeHelpKeep")
: this.$t("config.loadpoint.defaultModeHelp.charging");
},
minSocOptions(): SelectOption<number>[] {
// a list of entries from 0 to 95 with a step of 5
return Array.from(Array(20).keys())
.map((i) => i * 5)
.map(this.socOption);
},
limitSocOptions(): SelectOption<number>[] {
// a list of entries from 0 to 100 with a step of 5
return Array.from(Array(21).keys())
.map((i) => i * 5)
.map(this.socOption);
},
socSettingsVisible(): boolean {
return this.socBasedCharging || this.vehicleNotReachable;
},
},
watch: {
minSoc(value: number): void {
this.selectedMinSoc = value;
},
limitSoc(value: number): void {
this.selectedLimitSoc = value;
},
mode(value: string): void {
this.selectedMode = value;
},
},
methods: {
socOption(soc: number): SelectOption<number> {
return {
value: soc,
name: soc === 0 ? "---" : this.fmtSocOption(soc, this.rangePerSoc, distanceUnit()),
};
},
formId(name: string): string {
return `chargingplan_${this.id}_${name}`;
},
changeMinSoc(): void {
this.$emit("minsoc-updated", this.selectedMinSoc);
},
changeLimitSoc(): void {
this.$emit("limitsoc-updated", this.selectedLimitSoc);
},
changeMode(): void {
this.$emit("mode-updated", this.selectedMode);
},
},
});
</script>

View file

@ -8,7 +8,7 @@
>
<div class="value m-0 d-block align-items-baseline justify-content-center">
<button
class="value-button p-0"
class="value-button p-0 keyboard-focus-ring"
:class="buttonColor"
data-testid="charging-plan-button"
@click="openModal"

View file

@ -9,72 +9,33 @@
@open="modalVisible"
@closed="modalInvisible"
>
<div class="pt-2">
<ul v-if="vehicle" class="nav nav-tabs">
<li class="nav-item">
<a
class="nav-link"
:class="{ active: departureTabActive }"
href="#"
@click.prevent="showDepartureTab"
>
{{ $t("main.chargingPlan.departureTab") }}
</a>
</li>
<li v-if="vehicle" class="nav-item">
<a
class="nav-link"
:class="{ active: arrivalTabActive }"
href="#"
@click.prevent="showArrivalTab"
>
{{ $t("main.chargingPlan.arrivalTab") }}
</a>
</li>
</ul>
<div v-if="isModalVisible">
<PlansSettings
v-if="departureTabActive"
:id="id"
:staticPlan="staticPlan"
:repeatingPlans="repeatingPlans"
:effectiveLimitSoc="loadpoint?.effectiveLimitSoc"
:effectivePlanTime="loadpoint?.effectivePlanTime ?? undefined"
:effectivePlanSoc="loadpoint?.effectivePlanSoc"
:effectivePlanStrategy="loadpoint?.effectivePlanStrategy"
:planEnergy="loadpoint?.planEnergy"
:limitEnergy="loadpoint?.limitEnergy"
:socBasedPlanning="!!loadpoint?.socBasedPlanning"
:socPerKwh="loadpoint?.socPerKwh"
:rangePerSoc="loadpoint?.rangePerSoc"
:smartCostType="smartCostType"
:currency="currency"
:mode="loadpoint?.mode"
:capacity="vehicle?.capacity"
:vehicle="vehicle"
:vehicleLimitSoc="loadpoint?.vehicleLimitSoc"
:planOverrun="loadpoint?.planOverrun"
:forecast="forecast"
@static-plan-updated="updateStaticPlan"
@static-plan-removed="removeStaticPlan"
@repeating-plans-updated="updateRepeatingPlans"
@plan-strategy-updated="updatePlanStrategy"
/>
<Arrival
v-if="arrivalTabActive"
:id="id"
:minSoc="vehicle?.minSoc"
:limitSoc="vehicle?.limitSoc"
:mode="vehicle?.mode"
:vehicleName="vehicle?.name"
:vehicleNotReachable="loadpoint?.vehicleNotReachable"
:socBasedCharging="loadpoint?.socBasedCharging"
:rangePerSoc="loadpoint?.rangePerSoc"
@minsoc-updated="setMinSoc"
@limitsoc-updated="setLimitSoc"
@mode-updated="setMode"
/>
</div>
<div v-if="isModalVisible">
<PlansSettings
:id="id"
:staticPlan="staticPlan"
:repeatingPlans="repeatingPlans"
:effectiveLimitSoc="loadpoint?.effectiveLimitSoc"
:effectivePlanTime="loadpoint?.effectivePlanTime ?? undefined"
:effectivePlanSoc="loadpoint?.effectivePlanSoc"
:effectivePlanStrategy="loadpoint?.effectivePlanStrategy"
:planEnergy="loadpoint?.planEnergy"
:limitEnergy="loadpoint?.limitEnergy"
:socBasedPlanning="!!loadpoint?.socBasedPlanning"
:socPerKwh="loadpoint?.socPerKwh"
:rangePerSoc="loadpoint?.rangePerSoc"
:smartCostType="smartCostType"
:currency="currency"
:mode="loadpoint?.mode"
:capacity="vehicle?.capacity"
:vehicle="vehicle"
:vehicleLimitSoc="loadpoint?.vehicleLimitSoc"
:planOverrun="loadpoint?.planOverrun"
:forecast="forecast"
@static-plan-updated="updateStaticPlan"
@static-plan-removed="removeStaticPlan"
@repeating-plans-updated="updateRepeatingPlans"
@plan-strategy-updated="updatePlanStrategy"
/>
</div>
</GenericModal>
</Teleport>
@ -84,7 +45,6 @@
import { defineComponent, type PropType } from "vue";
import GenericModal from "../Helper/GenericModal.vue";
import PlansSettings from "./PlansSettings.vue";
import Arrival from "./Arrival.vue";
import api from "@/api";
import type {
PlanStrategy,
@ -93,21 +53,13 @@ import type {
StaticPlan,
StaticSocPlan,
} from "./types";
import type {
CHARGE_MODE,
CURRENCY,
Forecast,
SMART_COST_TYPE,
UiLoadpoint,
Vehicle,
} from "@/types/evcc";
import type { CURRENCY, Forecast, SMART_COST_TYPE, UiLoadpoint, Vehicle } from "@/types/evcc";
export default defineComponent({
name: "ChargingPlanModal",
components: {
GenericModal,
PlansSettings,
Arrival,
},
props: {
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
@ -119,7 +71,6 @@ export default defineComponent({
data() {
return {
isModalVisible: false,
activeTab: "departure",
id: undefined as string | number | undefined,
};
},
@ -151,18 +102,11 @@ export default defineComponent({
},
modalTitle(): string {
const baseTitle = this.$t("main.chargingPlan.modalTitle");
const withVehicle = this.arrivalTabActive || this.loadpoint?.socBasedPlanning;
if (withVehicle && this.vehicle) {
if (this.loadpoint?.socBasedPlanning && this.vehicle) {
return `${baseTitle}: ${this.vehicle.title}`;
}
return baseTitle;
},
departureTabActive(): boolean {
return this.activeTab === "departure" || !this.vehicle;
},
arrivalTabActive(): boolean {
return this.activeTab === "arrival" && !!this.vehicle;
},
apiVehicle(): string {
return `vehicles/${this.vehicle?.name}/`;
},
@ -192,19 +136,6 @@ export default defineComponent({
modalInvisible(): void {
this.isModalVisible = false;
},
setMinSoc(soc: number): void {
api.post(`${this.apiVehicle}minsoc/${soc}`);
},
setLimitSoc(soc: number): void {
api.post(`${this.apiVehicle}limitsoc/${soc}`);
},
setMode(mode: CHARGE_MODE | ""): void {
if (mode === "") {
api.delete(`${this.apiVehicle}mode`);
} else {
api.post(`${this.apiVehicle}mode/${mode}`);
}
},
updateStaticPlan(plan: StaticPlan): void {
const timeISO = plan.time.toISOString();
if (this.loadpoint?.socBasedPlanning) {
@ -232,12 +163,6 @@ export default defineComponent({
api.post(`${this.apiLoadpoint}plan/strategy`, strategy);
}
},
showDepartureTab(): void {
this.activeTab = "departure";
},
showArrivalTab(): void {
this.activeTab = "arrival";
},
},
});
</script>

View file

@ -1,5 +1,5 @@
<template>
<div class="mt-4">
<div>
<div class="form-group d-lg-flex align-items-baseline justify-content-between">
<div class="container px-0">
<ChargingPlanStaticSettings

View file

@ -16,9 +16,9 @@
<small v-if="expiryLabel(client)" class="text-muted ms-2 me-2 text-end">
{{ expiryLabel(client) }}
</small>
<span v-if="isActive(client)" class="badge bg-success ms-2 me-2">
<Badge v-if="isActive(client)" class="ms-2 me-2">
{{ $t("config.remote.active") }}
</span>
</Badge>
<small
v-else-if="activityLabel(client)"
class="text-muted ms-2 me-2 text-end d-block"
@ -52,6 +52,7 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import "@h2d2/shopicons/es/regular/trash";
import Badge from "../../Helper/Badge.vue";
import formatter from "@/mixins/formatter";
import minuteTicker from "@/mixins/minuteTicker";
import type { RemoteClient } from "@/types/evcc";
@ -59,6 +60,7 @@ import { isRemoteClientActive } from "@/utils/remote";
export default defineComponent({
name: "RemoteClientList",
components: { Badge },
mixins: [formatter, minuteTicker],
props: {
clients: { type: Array as PropType<RemoteClient[]>, required: true },

View file

@ -8,7 +8,15 @@
data-testid="energyflow"
@click="toggleDetails"
>
<div class="row">
<div
class="row keyboard-focus-ring"
:role="detailsAlwaysOpen ? undefined : 'button'"
:tabindex="detailsAlwaysOpen ? undefined : 0"
:aria-expanded="detailsAlwaysOpen ? undefined : detailsOpen"
:aria-label="detailsAlwaysOpen ? undefined : $t('main.energyflow.toggleDetails')"
@keydown.enter.prevent="toggleDetails"
@keydown.space.prevent="toggleDetails"
>
<Visualization
class="col-12 mb-3 mb-md-4"
:gridImport="gridImport"
@ -33,6 +41,7 @@
class="details"
:style="{ height: detailsHeight }"
:class="{ 'details--ready': ready }"
:inert="!detailsOpen && !detailsAlwaysOpen"
>
<div ref="detailsInner" class="details-inner row">
<div class="col-12 d-flex justify-content-between pt-2 mb-4">

View file

@ -0,0 +1,26 @@
import Badge from "./Badge.vue";
import type { Meta, StoryFn } from "@storybook/vue3";
const variants = ["success", "muted"] as const;
export default {
title: "Helper/Badge",
component: Badge,
argTypes: {
variant: { control: "select", options: variants },
},
} as Meta<typeof Badge>;
const Template: StoryFn<typeof Badge> = (args) => ({
components: { Badge },
setup() {
return { args };
},
template: '<Badge v-bind="args">connected to Garage</Badge>',
});
export const Success = Template.bind({});
Success.args = { variant: "success" };
export const Muted = Template.bind({});
Muted.args = { variant: "muted" };

View file

@ -0,0 +1,34 @@
<template>
<span class="badge rounded-pill" :class="`badge--${variant}`">
<slot />
</span>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
type Variant = "success" | "muted";
export default defineComponent({
name: "Badge",
props: {
variant: { type: String as PropType<Variant>, default: "success" },
},
});
</script>
<style scoped>
.badge {
--bs-badge-font-size: 0.75rem;
--bs-badge-padding-y: 0.25em;
font-weight: 700;
}
.badge--success {
background: var(--evcc-green);
color: var(--evcc-darkest-green);
}
.badge--muted {
background: var(--evcc-gray-10);
color: var(--evcc-gray);
}
</style>

View file

@ -1,11 +1,25 @@
<template>
<label
class="root position-relative"
:class="inline ? 'd-inline-block align-baseline' : 'd-block'"
class="root position-relative keyboard-focus-ring"
:class="[
inline ? 'd-inline-block align-baseline' : 'd-block',
{ 'focus-ring-visible': showFocusRing },
]"
:for="id"
role="button"
@mousedown="mouseDown = true"
>
<select :id="id" :value="selected" class="custom-select" tabindex="0" @change="change">
<select
:id="id"
:value="selected"
:aria-label="ariaLabel"
class="custom-select"
tabindex="0"
@change="change"
@focus="handleFocus"
@keydown="showFocusRing = true"
@blur="handleBlur"
>
<option
v-for="{ name, value, count, disabled } in options"
:key="value"
@ -30,8 +44,16 @@ export default defineComponent({
selected: { type: [String, Number] },
id: { type: String },
inline: Boolean,
ariaLabel: { type: String },
},
emits: ["change"],
data() {
return {
// selects match :focus-visible even on mouse interaction, track modality manually
mouseDown: false,
showFocusRing: false,
};
},
methods: {
text(name: string, count?: number) {
if (count === undefined) {
@ -42,6 +64,14 @@ export default defineComponent({
change(event: Event) {
this.$emit("change", event);
},
handleFocus() {
this.showFocusRing = !this.mouseDown;
this.mouseDown = false;
},
handleBlur() {
this.showFocusRing = false;
this.mouseDown = false;
},
},
});
</script>
@ -55,13 +85,7 @@ export default defineComponent({
position: absolute;
opacity: 0;
-webkit-appearance: menulist-button;
}
.root {
border-radius: var(--bs-border-radius);
}
.root:focus-within {
outline: var(--bs-focus-ring-width) solid var(--bs-focus-ring-color);
outline-width: var(--bs-focus-ring-width);
outline-offset: var(--bs-focus-ring-width);
/* WebKit propagates the select's intrinsic option width as overflow, causing page scroll */
contain: paint;
}
</style>

View file

@ -91,6 +91,7 @@ export default defineComponent({
methods: {
handleShow() {
this.$emit("open");
this.isModalVisible = true;
},
handleShown() {
this.$emit("opened");
@ -107,14 +108,19 @@ export default defineComponent({
}
});
}
this.isModalVisible = true;
// bootstrap removes the attribute on show, Vue only rewrites it on state change
(this.$refs["modal"] as HTMLElement)?.setAttribute("aria-hidden", "false");
},
handleHide() {
this.$emit("close");
this.isModalVisible = false;
},
handleHidden() {
// stale event from a previous close, modal was reopened while still fading out
if (this.isModalVisible) {
return;
}
this.$emit("closed");
this.isModalVisible = false;
if (this.configModalName) {
if (onModalHidden(this.configModalName)) {
this.$emit("dismiss");

View file

@ -1,6 +1,6 @@
<template>
<div class="root">
<div class="mb-2 label text-truncate" :class="labelClass" :style="{ color }">
<div class="label text-truncate" :class="labelClass" :style="{ color }">
<slot name="label">{{ label }}</slot>
</div>
<slot>
@ -54,6 +54,9 @@ export default defineComponent({
text-transform: uppercase;
color: var(--evcc-gray);
font-size: 14px;
/* focus ring clearance, keeps mb-2 spacing */
padding: 0.5rem;
margin: -0.5rem -0.5rem 0;
}
.value {
font-size: 18px;

View file

@ -0,0 +1,30 @@
<template>
<div class="mb-3 row">
<label :for="id" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ label }}
</label>
<div class="col-sm-8">
<div class="row">
<div class="col-lg-6">
<slot />
</div>
</div>
<small v-if="description" class="d-block text-muted mt-1">
{{ description }}
</small>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "SettingsFormRow",
props: {
id: String,
label: String,
description: String,
},
});
</script>

View file

@ -98,7 +98,7 @@
@remove-vehicle="removeVehicle"
@open-loadpoint-settings="openSettingsModal"
@batteryboost-updated="setBatteryBoost"
@open-modal="(openArrivalTab) => $emit('open-charging-plan-modal', openArrivalTab)"
@open-modal="$emit('open-charging-plan-modal')"
/>
</div>
</template>

View file

@ -36,9 +36,7 @@
class="h-100"
:class="{ 'loadpoint-unselected': !selected(loadpoint.id) }"
@click="goTo(loadpoint.id)"
@open-charging-plan-modal="
(openArrivalTab) => openChargingPlanModal(loadpoint.id, openArrivalTab)
"
@open-charging-plan-modal="openChargingPlanModal(loadpoint.id)"
@open-settings-modal="openSettingsModal(loadpoint.id)"
/>
</div>
@ -224,17 +222,10 @@ export default defineComponent({
}
}, 1000);
},
openChargingPlanModal(loadpointId: string, openArrivalTab = false) {
openChargingPlanModal(loadpointId: string) {
const modal = this.$refs["chargingPlanModal"] as
| InstanceType<typeof ChargingPlanModal>
| undefined;
if (openArrivalTab) {
modal?.showArrivalTab();
} else {
modal?.showDepartureTab();
}
modal?.open(loadpointId);
},
openSettingsModal(loadpointId: string) {

View file

@ -3,6 +3,7 @@
<LabelAndValue class="d-block" align="end">
<template #label>
<CustomSelect
inline
:selected="selectedKey"
:options="selectOptions"
data-testid="sessionInfoSelect"
@ -197,8 +198,10 @@ export default defineComponent({
<style scoped>
.sessionInfo * {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.sessionInfo .value {
cursor: pointer;
}
</style>

View file

@ -1,7 +1,7 @@
<template>
<button
type="button"
class="btn btn-sm btn-outline-secondary position-relative border-0 p-2 evcc-gray"
class="btn btn-sm btn-outline-secondary position-relative border-0 p-2 evcc-gray keyboard-focus-ring"
data-testid="loadpoint-settings-button"
>
<shopicon-regular-adjust size="s"></shopicon-regular-adjust>

View file

@ -89,7 +89,9 @@ import type {
FatalError,
EvOpt,
BATTERY_MODE,
Vehicle,
} from "@/types/evcc";
import vehicleList from "@/utils/vehicleList";
import store from "@/store";
import type { Grid } from "./types";
@ -130,7 +132,7 @@ export default defineComponent({
bufferSoc: Number,
bufferStartSoc: Number,
siteTitle: String,
vehicles: Object,
vehicles: Object as PropType<Record<string, Vehicle>>,
authProviders: { type: Object as PropType<AuthProviders>, default: () => ({}) },
currency: { type: String as PropType<CURRENCY> },
tariffFeedIn: Number,
@ -178,8 +180,7 @@ export default defineComponent({
return this.collectProps(Energyflow);
},
vehicleList() {
const vehicles = this.vehicles || {};
return Object.entries(vehicles).map(([name, vehicle]) => ({ name, ...vehicle }));
return vehicleList(this.vehicles);
},
showParkingLot() {
// work in progess

View file

@ -6,17 +6,7 @@
data-testid="limit-energy"
>
<h3 class="value m-0">
<label class="position-relative" role="button">
<select :value="limitEnergy" class="custom-select" @change="change">
<option
v-for="{ energy, text, disabled } in options"
:key="energy"
:value="energy"
:disabled="disabled"
>
{{ text }}
</option>
</select>
<CustomSelect inline :options="selectOptions" :selected="limitEnergy" @change="change">
<span
class="text-decoration-underline"
:class="{ 'text-gray fw-normal': !limitEnergy }"
@ -24,7 +14,7 @@
>
<AnimatedNumber :to="limitEnergy" :format="fmtEnergy" />
</span>
</label>
</CustomSelect>
<div v-if="estimated" class="extraValue text-nowrap">
<AnimatedNumber :to="estimated" :format="fmtSoc" />
@ -36,13 +26,14 @@
<script lang="ts">
import LabelAndValue from "../Helper/LabelAndValue.vue";
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
import CustomSelect from "../Helper/CustomSelect.vue";
import formatter from "@/mixins/formatter";
import { estimatedSoc, energyOptions, optionStep, fmtEnergy } from "@/utils/energyOptions.ts";
import { defineComponent } from "vue";
export default defineComponent({
name: "LimitEnergySelect",
components: { LabelAndValue, AnimatedNumber },
components: { LabelAndValue, AnimatedNumber, CustomSelect },
mixins: [formatter],
props: {
limitEnergy: { type: Number, default: 0 },
@ -62,6 +53,13 @@ export default defineComponent({
this.socPerKwh
);
},
selectOptions() {
return this.options.map(({ energy, text, disabled }) => ({
value: energy,
name: text,
disabled,
}));
},
step() {
return optionStep(this.capacity || 100);
},
@ -96,13 +94,4 @@ export default defineComponent({
font-size: 14px;
font-weight: normal;
}
.custom-select {
left: 0;
top: 0;
bottom: 0;
right: 0;
cursor: pointer;
position: absolute;
opacity: 0;
}
</style>

View file

@ -1,16 +1,11 @@
<template>
<LabelAndValue class="flex-grow-1" :label="title" align="end" data-testid="limit-soc">
<h3 class="value m-0">
<label class="position-relative" role="button">
<select :value="limitSoc" class="custom-select" @change="change">
<option v-for="{ soc, text } in options" :key="soc" :value="soc">
{{ text }}
</option>
</select>
<CustomSelect inline :options="options" :selected="limitSoc" @change="change">
<span class="text-decoration-underline" data-testid="limit-soc-value">
<AnimatedNumber :to="limitSoc" :format="formatSoc" />
</span>
</label>
</CustomSelect>
<div v-if="estimatedTargetRange" class="extraValue text-nowrap">
<AnimatedNumber :to="estimatedTargetRange" :format="formatKm" />
@ -23,12 +18,13 @@
import { defineComponent } from "vue";
import LabelAndValue from "../Helper/LabelAndValue.vue";
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
import CustomSelect from "../Helper/CustomSelect.vue";
import { distanceUnit } from "@/units";
import formatter from "@/mixins/formatter";
export default defineComponent({
name: "LimitSocSelect",
components: { LabelAndValue, AnimatedNumber },
components: { LabelAndValue, AnimatedNumber, CustomSelect },
mixins: [formatter],
props: {
limitSoc: { type: Number, default: 0 },
@ -47,8 +43,8 @@ export default defineComponent({
const start = this.rangeActive ? this.minTemp : 20;
const end = this.rangeActive ? this.maxTemp : 100;
for (let soc = start; soc <= end; soc += this.step) {
const text = this.fmtSocOption(soc, this.rangePerSoc, distanceUnit(), this.heating);
result.push({ soc, text });
const name = this.fmtSocOption(soc, this.rangePerSoc, distanceUnit(), this.heating);
result.push({ value: soc, name });
}
return result;
},
@ -90,7 +86,6 @@ export default defineComponent({
<style scoped>
.value {
font-size: 18px;
overflow: hidden;
}
.extraValue {
margin-top: 0.1rem;
@ -98,14 +93,4 @@ export default defineComponent({
font-size: 14px;
font-weight: normal;
}
.custom-select {
left: 0;
top: 0;
bottom: 0;
right: 0;
cursor: pointer;
position: absolute;
opacity: 0;
max-width: 100%;
}
</style>

View file

@ -5,7 +5,13 @@
role="button"
data-testid="change-vehicle"
>
<select :id="dropdownId" :value="selected" class="custom-select" @change="change">
<select
:id="dropdownId"
:value="selected"
:aria-label="$t('main.vehicle.changeVehicle')"
class="custom-select"
@change="change"
>
<option
v-for="{ name, value } in vehicleOptions"
:key="name"

View file

@ -0,0 +1,207 @@
<template>
<GenericModal
id="vehicleSettingsModal"
ref="modal"
size="lg"
:title="$t('main.vehicleSettings.title')"
data-testid="vehicle-settings-modal"
@open="modalVisible"
@closed="modalInvisible"
>
<div v-if="isModalVisible">
<p class="text-gray mt-0 mb-4">
{{ $t("main.vehicleSettings.description") }}
<a :href="docsUrl" target="_blank" rel="noopener">
{{ $t("main.vehicleSettings.learnMore") }}
</a>
</p>
<div
v-for="(vehicle, index) in vehicles"
:key="vehicle.name"
role="group"
:aria-label="vehicle.title"
:class="{ 'border-top pt-4': index > 0 }"
class="mb-4"
>
<div class="d-flex flex-wrap align-items-center column-gap-2 row-gap-1 mb-3">
<VehicleIcon :name="vehicle.icon" class="flex-shrink-0" />
<strong class="text-truncate">{{ vehicle.title }}</strong>
<Badge v-if="connectedLoadpoint(vehicle)" class="flex-shrink-0">
{{
$t("main.vehicleSettings.connectedTo", [
connectedLoadpoint(vehicle)?.title ||
$t("main.loadpoint.fallbackName"),
])
}}
</Badge>
<Badge v-else variant="muted" class="flex-shrink-0">
{{ $t("main.vehicleSettings.notConnected") }}
</Badge>
</div>
<SettingsFormRow
:id="fieldId(vehicle, 'mode')"
:label="$t('main.vehicleSettings.mode')"
>
<select
:id="fieldId(vehicle, 'mode')"
class="form-select form-select-sm"
:value="vehicle.mode ?? ''"
@change="changeMode(vehicle, $event)"
>
<option v-for="opt in modeOptions" :key="opt.value" :value="opt.value">
{{ opt.name }}
</option>
</select>
</SettingsFormRow>
<template v-if="socSupported(vehicle)">
<SettingsFormRow
:id="fieldId(vehicle, 'limitSoc')"
:label="$t('main.vehicleSettings.limitSoc')"
>
<select
:id="fieldId(vehicle, 'limitSoc')"
class="form-select form-select-sm"
:value="vehicle.limitSoc ?? 0"
@change="changeLimitSoc(vehicle, $event)"
>
<option
v-for="opt in socOptions(vehicle)"
:key="opt.value"
:value="opt.value"
>
{{ opt.name }}
</option>
</select>
</SettingsFormRow>
<SettingsFormRow
:id="fieldId(vehicle, 'minSoc')"
:label="$t('main.vehicleSettings.minSoc')"
:description="$t('main.vehicleSettings.minSocDescription')"
>
<select
:id="fieldId(vehicle, 'minSoc')"
class="form-select form-select-sm"
:value="vehicle.minSoc ?? 0"
@change="changeMinSoc(vehicle, $event)"
>
<option
v-for="opt in socOptions(vehicle)"
:key="opt.value"
:value="opt.value"
>
{{ opt.name }}
</option>
</select>
</SettingsFormRow>
</template>
</div>
<p class="mb-0 border-top pt-4">
<i18n-t keypath="main.vehicleSettings.editHint" tag="span" scope="global">
<router-link to="/config#vehicles" @click="closeModal">
{{ $t("config.main.title") }}
</router-link>
</i18n-t>
</p>
</div>
</GenericModal>
</template>
<script lang="ts">
import Badge from "../Helper/Badge.vue";
import GenericModal from "../Helper/GenericModal.vue";
import SettingsFormRow from "../Helper/SettingsFormRow.vue";
import VehicleIcon from "../VehicleIcon";
import api from "@/api";
import formatter from "@/mixins/formatter";
import { docsPrefix } from "@/i18n";
import { distanceUnit } from "@/units";
import { vehicleHasSoc, vehicleNotReachable } from "@/uiLoadpoints";
import { CHARGE_MODE, type SelectOption, type UiLoadpoint, type Vehicle } from "@/types/evcc";
import { defineComponent, type PropType } from "vue";
const { OFF, PV, MINPV, NOW } = CHARGE_MODE;
export default defineComponent({
name: "VehicleSettingsModal",
components: { Badge, GenericModal, SettingsFormRow, VehicleIcon },
mixins: [formatter],
props: {
vehicles: { type: Array as PropType<Vehicle[]>, default: () => [] },
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
},
data() {
return {
isModalVisible: false,
};
},
computed: {
docsUrl(): string {
return `${docsPrefix()}/features/limits`;
},
modeOptions(): SelectOption<string>[] {
return [
{ value: "", name: this.$t("main.vehicleSettings.keepAsIs") },
...[OFF, PV, MINPV, NOW].map((mode) => ({
value: mode,
name: this.$t(`main.mode.${mode}`),
})),
];
},
},
methods: {
modalVisible(): void {
this.isModalVisible = true;
},
modalInvisible(): void {
this.isModalVisible = false;
},
closeModal(): void {
(this.$refs["modal"] as InstanceType<typeof GenericModal> | undefined)?.close();
},
fieldId(vehicle: Vehicle, field: string): string {
return `vehicleSettings-${vehicle.name}-${field}`;
},
connectedLoadpoint(vehicle: Vehicle): UiLoadpoint | undefined {
return this.loadpoints.find((lp) => lp.vehicleName === vehicle.name && lp.connected);
},
socSupported(vehicle: Vehicle): boolean {
const loadpoint = this.connectedLoadpoint(vehicle);
if (loadpoint) {
return loadpoint.socBasedCharging || loadpoint.vehicleNotReachable;
}
return vehicleHasSoc(vehicle) || vehicleNotReachable(vehicle);
},
socOptions(vehicle: Vehicle): SelectOption<number>[] {
// 0 = none, then 5-100 in steps of 5
const rangePerSoc = this.connectedLoadpoint(vehicle)?.rangePerSoc;
return Array.from(Array(21).keys()).map((i) => {
const soc = i * 5;
return {
value: soc,
name:
soc === 0
? this.$t("general.none")
: this.fmtSocOption(soc, rangePerSoc, distanceUnit()),
};
});
},
selectValue(event: Event): string {
return (event.target as HTMLSelectElement).value;
},
changeMode(vehicle: Vehicle, event: Event): void {
const mode = this.selectValue(event);
if (mode === "") {
api.delete(`vehicles/${vehicle.name}/mode`);
} else {
api.post(`vehicles/${vehicle.name}/mode/${mode}`);
}
},
changeMinSoc(vehicle: Vehicle, event: Event): void {
api.post(`vehicles/${vehicle.name}/minsoc/${this.selectValue(event)}`);
},
changeLimitSoc(vehicle: Vehicle, event: Event): void {
api.post(`vehicles/${vehicle.name}/limitsoc/${this.selectValue(event)}`);
},
},
});
</script>

View file

@ -1,6 +1,6 @@
<template>
<div class="d-flex justify-content-between mb-3 align-items-center" data-testid="vehicle-title">
<h4 class="d-flex align-items-center m-0 flex-grow-1 overflow-hidden">
<h4 class="d-flex align-items-center flex-grow-1 overflow-hidden ring-space">
<div
v-if="iconType === 'refresh'"
ref="refresh"
@ -158,6 +158,11 @@ export default defineComponent({
</script>
<style scoped>
/* overflow-hidden would clip the vehicle select focus ring */
.ring-space {
padding: 0.5rem;
margin: -0.5rem;
}
.vehicle-name {
text-decoration-color: var(--evcc-gray);
}

View file

@ -10,8 +10,8 @@
v-bind="vehicleStatus"
class="mb-2"
@open-loadpoint-settings="$emit('open-loadpoint-settings')"
@open-minsoc-settings="openPlanModal(true)"
@open-plan-modal="openPlanModal"
@open-minsoc-settings="openVehicleSettings"
@open-plan-modal="$emit('open-modal')"
/>
<div class="mt-2 mb-4 d-flex gap-2">
<BatteryBoostButton
@ -26,7 +26,7 @@
v-bind="vehicleSocProps"
@limit-soc-updated="limitSocUpdated"
@limit-soc-drag="limitSocDrag"
@plan-clicked="openPlanModal"
@plan-clicked="$emit('open-modal')"
/>
</div>
<div class="details d-flex flex-wrap justify-content-between">
@ -80,6 +80,7 @@
</template>
<script lang="ts">
import Modal from "bootstrap/js/dist/modal";
import collector from "@/mixins/collector.ts";
import formatter, { POWER_UNIT } from "@/mixins/formatter";
import LabelAndValue from "../Helper/LabelAndValue.vue";
@ -287,8 +288,10 @@ export default defineComponent({
fmtEnergy(value: number) {
return this.fmtWh(value, value == 0 ? POWER_UNIT.KW : POWER_UNIT.AUTO);
},
openPlanModal(openArrivalTab = false) {
this.$emit("open-modal", openArrivalTab);
openVehicleSettings() {
Modal.getOrCreateInstance(
document.getElementById("vehicleSettingsModal") as HTMLElement
).show();
},
handleBoostStatus(status: VehicleStatus) {
this.statusOverride = status;

View file

@ -9,6 +9,15 @@ const get = (id: string) => {
return settings.loadpoints[id];
};
export const vehicleHasSoc = (vehicle?: Vehicle): boolean => {
return !vehicle?.features?.includes("Offline");
};
export const vehicleNotReachable = (vehicle?: Vehicle): boolean => {
const features = vehicle?.features || [];
return features.includes("Offline") && features.includes("Retryable");
};
export const convertToUiLoadpoints = (
loadpoints: Loadpoint[],
vehicles: Record<string, Vehicle>
@ -24,8 +33,8 @@ export const convertToUiLoadpoints = (
const capacity = vehicle?.capacity || 0;
const range = distanceValue(vehicleRange);
const vehicleKnown = !!lp.vehicleName;
const vehicleHasSoc = vehicleKnown && !vehicle?.features?.includes("Offline");
const socBasedCharging = vehicleHasSoc || lp.vehicleSoc > 0;
const hasSoc = vehicleKnown && vehicleHasSoc(vehicle);
const socBasedCharging = hasSoc || lp.vehicleSoc > 0;
const socBasedPlanning = !!(socBasedCharging && capacity > 0);
return {
@ -36,7 +45,7 @@ export const convertToUiLoadpoints = (
vehicleSoc,
capacity,
vehicleKnown,
vehicleHasSoc,
vehicleHasSoc: hasSoc,
socBasedCharging,
socBasedPlanning,
displayTitle: vehicle?.title || lp.title || "Charging point",
@ -49,9 +58,7 @@ export const convertToUiLoadpoints = (
rangePerSoc:
vehicleSoc > 10 && range ? Math.round((range / vehicleSoc) * 1e2) / 1e2 : undefined,
socPerKwh: capacity > 0 ? 100 / capacity : 0,
vehicleNotReachable:
(vehicle?.features || []).includes("Offline") &&
(vehicle?.features || []).includes("Retryable"),
vehicleNotReachable: vehicleNotReachable(vehicle),
} satisfies UiLoadpoint;
});

View file

@ -0,0 +1,6 @@
import type { Vehicle } from "@/types/evcc";
// convert the vehicles state record into a list, keys become vehicle names
export default function vehicleList(vehicles?: Record<string, Vehicle>): Vehicle[] {
return Object.entries(vehicles || {}).map(([name, vehicle]) => ({ ...vehicle, name }));
}

View file

@ -9,6 +9,7 @@
<BottomTabBar v-bind="bottomTabBarProps" />
<GlobalSettingsModal v-bind="globalSettingsProps" />
<VehicleSettingsModal :vehicles="vehicleList" :loadpoints="state.uiLoadpoints" />
<AboutModal v-bind="aboutModalProps" />
<HelpModal />
<PasswordModal />
@ -21,12 +22,14 @@
import store from "../store";
import BottomTabBar from "../components/BottomTabs/Bar.vue";
import GlobalSettingsModal from "../components/GlobalSettings/GlobalSettingsModal.vue";
import VehicleSettingsModal from "../components/Vehicles/SettingsModal.vue";
import OfflineIndicator from "../components/Footer/OfflineIndicator.vue";
import PasswordModal from "../components/Auth/PasswordModal.vue";
import LoginModal from "../components/Auth/LoginModal.vue";
import AboutModal from "../components/AboutModal.vue";
import HelpModal from "../components/HelpModal.vue";
import collector from "../mixins/collector";
import vehicleList from "@/utils/vehicleList";
import { defineComponent } from "vue";
// assume offline if not data received for 5 minutes
@ -45,6 +48,7 @@ export default defineComponent({
AboutModal,
BottomTabBar,
GlobalSettingsModal,
VehicleSettingsModal,
HelpModal,
PasswordModal,
LoginModal,
@ -77,6 +81,9 @@ export default defineComponent({
const { state, uiLoadpoints } = store;
return { ...state, uiLoadpoints: uiLoadpoints.value };
},
vehicleList() {
return vehicleList(this.state.vehicles);
},
globalSettingsProps() {
return this.collectProps(GlobalSettingsModal, this.state);
},

View file

@ -55,7 +55,7 @@
/>
</div>
<h2 class="my-4">{{ $t("config.section.vehicles") }}</h2>
<h2 id="vehicles" class="my-4">{{ $t("config.section.vehicles") }}</h2>
<div class="p-0 config-list box-pull-out">
<DeviceCard
v-for="vehicle in vehicles"

View file

@ -215,6 +215,7 @@ import DynamicPriceIcon from "../components/MaterialIcon/DynamicPrice.vue";
import { TYPES, GROUPS, PERIODS, type Session } from "../components/Sessions/types";
import { defineComponent, type PropType } from "vue";
import { CURRENCY, type Notification } from "@/types/evcc";
import vehicleList from "@/utils/vehicleList";
export default defineComponent({
name: "Sessions",
@ -468,8 +469,7 @@ export default defineComponent({
});
},
vehicleList() {
const vehicles = store.state.vehicles || {};
return Object.entries(vehicles).map(([name, vehicle]) => ({ ...vehicle, name }));
return vehicleList(store.state.vehicles);
},
loadpointList() {
const loadpoints = store.state.loadpoints || [];

View file

@ -854,9 +854,7 @@
"chargingPlan": {
"active": "Активен",
"addRepeatingPlan": "Добавяне на повтарящ се план",
"arrivalTab": "Пристигане",
"day": "Ден",
"departureTab": "Заминаване",
"goal": "Цел на зареждане",
"modalTitle": "План за зареждане",
"none": "няма",
@ -880,8 +878,6 @@
"strategySettings": "Настройки на стратегията",
"time": "Време",
"title": "План",
"titleMinSoc": "Минимално зареждане",
"titleTargetCharge": "Заминаване",
"unsavedChanges": "Има незаписани промени. Приложи сега?",
"update": "Приложи",
"weekdays": "Дни"
@ -939,20 +935,12 @@
"currents": "Заряден ток",
"default": "по подразбиране",
"disclaimerHint": "Забележка:",
"limitSoc": {
"description": "Ограничение на зареждането, което се използва, когато този автомобил е свързан.",
"label": "Лимит по подразбиране"
},
"maxCurrent": {
"label": "Макс. ток"
},
"minCurrent": {
"label": "Мин. ток"
},
"minSoc": {
"description": "Превозното средство се зарежда „бързо“ до {0} в соларен режим. След това продължава със соларен излишък. Полезно за осигуряване на минимален пробег дори в по-тъмни дни.",
"label": "Мин. заряд %"
},
"onlyForSocBasedCharging": "Тези опции са налични само за превозни средства с известен заряд.",
"phasesConfigured": {
"label": "Фази",

View file

@ -393,9 +393,6 @@
"remoteDisabledSoft": "{source}: isključeno prilagodljivo solarno punjenje"
},
"loadpointSettings": {
"minSoc": {
"description": "Vozilo se brzo puni do {0} u solarnom načinu rada. Zatim nastavlja punjenjem viškom solarne energije. Korisno za osiguranje minimalnog dometa čak i za tmurnije dane."
},
"phasesConfigured": {
"phases_1_hint": "(od {min} do {max})",
"phases_3_hint": "(od {min} do {max})"

View file

@ -116,13 +116,9 @@
},
"main": {
"chargingPlan": {
"arrivalTab": "Arribades",
"departureTab": "Sortides",
"modalTitle": "Plà de càrrega",
"none": "no definit",
"title": "Plà",
"titleMinSoc": "Càrrega mínima",
"titleTargetCharge": "Sortides"
"title": "Plà"
},
"energyflow": {
"battery": "Bateria",
@ -156,10 +152,6 @@
"minCurrent": {
"label": "Corriente de carrega mínima"
},
"minSoc": {
"description": "Marge per a emergències. El vehicle es carrega 'ràpidament' a {0} en mode solar. Després continua amb l'excedent fotovoltaic.",
"label": "Càrrega de min. %"
},
"phasesConfigured": {
"label": "Fases",
"phases_0": "Canvi automàtic",

View file

@ -1157,9 +1157,7 @@
"chargingPlan": {
"active": "Aktivní",
"addRepeatingPlan": "Přidat opakující se plán",
"arrivalTab": "Příchod",
"day": "Den",
"departureTab": "Odchod",
"goal": "Cílový stav nabíjení",
"modalTitle": "Plán nabíjení",
"none": "Žádný",
@ -1183,8 +1181,6 @@
"strategySettings": "Nastavení strategie",
"time": "Čas",
"title": "Plán",
"titleMinSoc": "Min. nabíjení",
"titleTargetCharge": "Odjezd",
"unsavedChanges": "Jsou zde neuložené změny. Použít nyní?",
"update": "Nastavit",
"weekdays": "Dny"
@ -1282,20 +1278,12 @@
"currents": "Nabíjecí proud",
"default": "výchozí",
"disclaimerHint": "Poznámka:",
"limitSoc": {
"description": "Nabíjecí limit, který je použit, když je připojeno toto vozidlo.",
"label": "Výchozí limit nabití"
},
"maxCurrent": {
"label": "Max. Proud"
},
"minCurrent": {
"label": "Min. Proud"
},
"minSoc": {
"description": "Vozidlo bude rychle nabito na {0} v solárním režimu. Poté bude pokračovat nabíjení z přebytku solární energie. Užitečné pro zajištění minimálního dojezdu i během zatažených dnů.",
"label": "Minimální úroveň nabití"
},
"onlyForSocBasedCharging": "Tyto volby jsou povolené pouze pro vozidla se známou nabíjecí úrovní.",
"phasesConfigured": {
"label": "Připojení fází",

View file

@ -1194,9 +1194,7 @@
"chargingPlan": {
"active": "Aktiv",
"addRepeatingPlan": "Tilføj plan for gentagelse",
"arrivalTab": "Ankomst",
"day": "Dag",
"departureTab": "Afgang",
"goal": "Opladnings mål",
"modalTitle": "Lade plan",
"none": "ingen",
@ -1220,8 +1218,6 @@
"strategySettings": "Indstillinger for strategi",
"time": "Tid",
"title": "Plan",
"titleMinSoc": "Min. ladning",
"titleTargetCharge": "Afgang",
"unsavedChanges": "Der er ændringer som ikke er gemt. Skal de benyttes nu?",
"update": "Anvend",
"weekdays": "Dage"
@ -1320,20 +1316,12 @@
"currents": "Ladestrøm",
"default": "standard",
"disclaimerHint": "Bemærk:",
"limitSoc": {
"description": "Opladningsgrænse som benyttes når dette køretøj er forbundet.",
"label": "standard opladningsgrænse"
},
"maxCurrent": {
"label": "Max. Nuværende"
},
"minCurrent": {
"label": "Min. Nuværende"
},
"minSoc": {
"description": "Køretøjet bliver „hurtigt” opladet til {0} i solar mode, og fortsætter derefter med solenergioverskuddet. Nyttig til at sikre en minimum rækkevidde selv på mørke dage.",
"label": "Min. opladning %"
},
"onlyForSocBasedCharging": "Disse indstillinger er kun tilgængelig for køretøjer med kendt opladningsniveau.",
"phasesConfigured": {
"label": "Faser",

View file

@ -1146,6 +1146,7 @@
},
"general": {
"download": "Download",
"none": "keine",
"note": "Hinweis:"
},
"header": {
@ -1259,9 +1260,7 @@
"chargingPlan": {
"active": "Aktiv",
"addRepeatingPlan": "Wiederholenden Plan hinzufügen",
"arrivalTab": "Ankunft",
"day": "Tag",
"departureTab": "Abfahrt",
"goal": "Ladeziel",
"modalTitle": "Ladeplanung",
"none": "keiner",
@ -1285,8 +1284,6 @@
"strategySettings": "Strategie-Einstellungen",
"time": "Zeit",
"title": "Plan",
"titleMinSoc": "Min. Ladung",
"titleTargetCharge": "Abfahrt",
"unsavedChanges": "Ungespeicherte Änderungen vorhanden. Jetzt anwenden?",
"update": "Anwenden",
"weekdays": "Tage"
@ -1315,7 +1312,8 @@
"pv": "Solaranlage",
"pvExport": "Einspeisung",
"pvProduction": "Erzeugung",
"selfConsumption": "Eigenverbrauch"
"selfConsumption": "Eigenverbrauch",
"toggleDetails": "Energiefluss-Details"
},
"heatingStatus": {
"charging": "Heize …",
@ -1395,20 +1393,12 @@
"currents": "Ladestrom",
"default": "default",
"heating": "Heizen",
"limitSoc": {
"description": "Dieses Ladelimit wird verwendet, wenn das Fahrzeug angeschlossen wird.",
"label": "Standard Ladelimit"
},
"maxCurrent": {
"label": "Max. Ladestrom"
},
"minCurrent": {
"label": "Min. Ladestrom"
},
"minSoc": {
"description": "Fahrzeug wird im PV-Modus „Schnell“ auf {0} geladen. Danach weiter mit PV-Überschuss. Nützlich, um auch an dunklen Tagen eine Mindestreichweite zu gewährleisten.",
"label": "Min. Ladung %"
},
"minTemp": {
"description": "Unter {0} wird im PV-Modus auch mit Batterie- und Netzstrom geheizt, um diese Temperatur zu halten.",
"label": "Min. Temperatur"
@ -1495,6 +1485,20 @@
"unknown": "Gastfahrzeug",
"vehicleSoc": "Ladestand"
},
"vehicleSettings": {
"connectedTo": "verbunden mit {0}",
"description": "Diese Standardwerte werden angewendet, wenn das Fahrzeug an einem Ladepunkt angeschlossen wird.",
"editHint": "Fahrzeuge unter {0} hinzufügen oder bearbeiten.",
"keepAsIs": "Beibehalten",
"learnMore": "Mehr erfahren.",
"limitSoc": "Standardlimit",
"menu": "Fahrzeuge",
"minSoc": "Minimale Ladung",
"minSocDescription": "Wird im PV-Modus schnell bis zu diesem Wert geladen.",
"mode": "Standardmodus",
"notConnected": "nicht verbunden",
"title": "Fahrzeugeinstellungen"
},
"vehicleStatus": {
"awaitingAuthorization": "Warte auf Autorisierung.",
"batteryBoost": "Batterie Boost aktiv.",

View file

@ -859,9 +859,7 @@
"chargingPlan": {
"active": "Ενεργό",
"addRepeatingPlan": "Προσθήκη επαναλαμβανόμενου προγράμματος",
"arrivalTab": "Άφιξη",
"day": "Ημέρα",
"departureTab": "Αναχώρηση",
"goal": "Επιθυμητή φόρτιση",
"modalTitle": "Πρόγραμμα Φόρτισης",
"none": "κανένα",
@ -885,8 +883,6 @@
"strategySettings": "Ρυθμίσεις στρατηγικής",
"time": "Ώρα",
"title": "Πρόγραμμα",
"titleMinSoc": "Ελάχιστη φόρτιση",
"titleTargetCharge": "Αναχώρηση",
"unsavedChanges": "Υπάρχουν μη αποθηκευμένες αλλαγές. Υποβολή τώρα;",
"update": "Υποβολή",
"weekdays": "Ημέρες"
@ -944,20 +940,12 @@
"currents": "Ένταση φόρτισης",
"default": "προεπιλογή",
"disclaimerHint": "Σημείωση:",
"limitSoc": {
"description": "Όριο φόρτισης που χρησιμοποιείται όταν αυτό το όχημα είναι συνδεδεμένο.",
"label": "Προεπιλεγμένο όριο"
},
"maxCurrent": {
"label": "Μέγιστη Ένταση"
},
"minCurrent": {
"label": "Ελάχιστη Ένταση"
},
"minSoc": {
"description": "Το όχημα φορτίζεται (γρήγορα) μέχρι {0} σε λειτουργία ηλιακής Φ/Β ενέργειας. Μετά συνεχίζει μόνο με το πλεόνασμα Φ/Β ενέργειας. Χρήσιμο για να εξασφαλίσει μια ελάχιστη εμβέλεια ακόμη και για πιο συννεφιασμένες ημέρες.",
"label": "Ελ. % φόρτισης"
},
"onlyForSocBasedCharging": "Αυτές οι επιλογές είναι διαθέσιμες μόνο για οχήματα με γνωστό επίπεδο φόρτισης.",
"phasesConfigured": {
"label": "Φάσεις",

View file

@ -1146,6 +1146,7 @@
},
"general": {
"download": "Download",
"none": "none",
"note": "Note:"
},
"header": {
@ -1259,9 +1260,7 @@
"chargingPlan": {
"active": "Active",
"addRepeatingPlan": "Add repeating plan",
"arrivalTab": "Arrival",
"day": "Day",
"departureTab": "Departure",
"goal": "Charging goal",
"modalTitle": "Charging Plan",
"none": "none",
@ -1285,8 +1284,6 @@
"strategySettings": "Strategy settings",
"time": "Time",
"title": "Plan",
"titleMinSoc": "Min charge",
"titleTargetCharge": "Departure",
"unsavedChanges": "There are unsaved changes. Apply now?",
"update": "Apply",
"weekdays": "Days"
@ -1315,7 +1312,8 @@
"pv": "Solar system",
"pvExport": "Grid export",
"pvProduction": "Production",
"selfConsumption": "Self-consumption"
"selfConsumption": "Self-consumption",
"toggleDetails": "Energy flow details"
},
"heatingStatus": {
"charging": "Heating…",
@ -1395,20 +1393,12 @@
"currents": "Charging Current",
"default": "default",
"heating": "Heating",
"limitSoc": {
"description": "Charging limit that is used when this vehicle is connected.",
"label": "Default limit"
},
"maxCurrent": {
"label": "Max. Current"
},
"minCurrent": {
"label": "Min. Current"
},
"minSoc": {
"description": "The vehicle gets „fast” charged to {0} in solar mode. Then continues with solar surplus. Useful to ensure a minimum range even for darker days.",
"label": "Min. charge %"
},
"minTemp": {
"description": "Below {0} battery and grid power are also used in solar mode to maintain this temperature.",
"label": "Min. temperature"
@ -1495,6 +1485,20 @@
"unknown": "Guest vehicle",
"vehicleSoc": "Charge"
},
"vehicleSettings": {
"connectedTo": "connected to {0}",
"description": "These defaults are applied when the vehicle connects to a charging point.",
"editHint": "Add or edit vehicles in {0}.",
"keepAsIs": "Keep as is",
"learnMore": "Learn more.",
"limitSoc": "Default limit",
"menu": "Vehicles",
"minSoc": "Minimum charge",
"minSocDescription": "Fast charge to this level in solar mode.",
"mode": "Default mode",
"notConnected": "not connected",
"title": "Vehicle Settings"
},
"vehicleStatus": {
"awaitingAuthorization": "Waiting for authorization.",
"batteryBoost": "Battery boost active.",

View file

@ -988,9 +988,7 @@
"chargingPlan": {
"active": "Activo",
"addRepeatingPlan": "Añadir plan recurrente",
"arrivalTab": "Llegadas",
"day": "Día",
"departureTab": "Salidas",
"goal": "Objetivo de la carga",
"modalTitle": "Plan de carga",
"none": "ninguno",
@ -1014,8 +1012,6 @@
"strategySettings": "Configuración de la estrategia",
"time": "Tiempo",
"title": "Plan",
"titleMinSoc": "Carga mínima",
"titleTargetCharge": "Partida",
"unsavedChanges": "Hay cambios sin guardar. ¿Aplicar ahora?",
"update": "Aplicar",
"weekdays": "Días"
@ -1073,20 +1069,12 @@
"currents": "Corriente de carga",
"default": "defecto",
"disclaimerHint": "Nota:",
"limitSoc": {
"description": "Límite de carga que se utiliza cuando este vehículo está conectado.",
"label": "Límite predeterminado"
},
"maxCurrent": {
"label": "Corriente de carga máxima"
},
"minCurrent": {
"label": "Corriente de carga mínima"
},
"minSoc": {
"description": "El vehículo se carga «rápidamente» hasta {0} en modo solar. A continuación, continúa con el excedente solar. Útil para garantizar una autonomía mínima incluso en días más oscuros.",
"label": "Carga de min. %"
},
"onlyForSocBasedCharging": "Estas opciones solo están disponibles para vehículos con un nivel de carga conocido.",
"phasesConfigured": {
"label": "Fases",

View file

@ -1192,9 +1192,7 @@
"chargingPlan": {
"active": "Aktiivinen",
"addRepeatingPlan": "Lisää toistuva suunnitelma",
"arrivalTab": "Saapuminen",
"day": "Päivä",
"departureTab": "Lähtö",
"goal": "Lataus tavoite",
"modalTitle": "Lataussuunnitelma",
"none": "ei mitään",
@ -1218,8 +1216,6 @@
"strategySettings": "Strategiamääritykset",
"time": "Aika",
"title": "Suunnitelma",
"titleMinSoc": "Minimi lataus",
"titleTargetCharge": "Lähtö",
"unsavedChanges": "Tallentamattomia muutoksia. Asetetaanko nyt?",
"update": "Aseta",
"weekdays": "Päivät"
@ -1317,20 +1313,12 @@
"currents": "Latausvirta",
"default": "oletus",
"disclaimerHint": "Huomautus:",
"limitSoc": {
"description": "Latausraja mitä käytetään kun ajoneuvo yhdistetty.",
"label": "Oletusraja"
},
"maxCurrent": {
"label": "Maksimivirta"
},
"minCurrent": {
"label": "Minimivirta"
},
"minSoc": {
"description": "Ajoneuvo ladataan välittömästi {0} asti huolimatta aurinkoenergian ylijäämästä, tämän jälkeen vain ylijäämäenergialla. Tämä on hyödyllinen takaamaan minimi toimintamatka myös pimeinä päivinä.",
"label": "Minimi lataustaso"
},
"onlyForSocBasedCharging": "Nämä vaihtoehdot ovat käytettävissä vain ajoneuvoille, joiden lataustaso on tiedossa.",
"phasesConfigured": {
"label": "Vaihetila",

View file

@ -1196,9 +1196,7 @@
"chargingPlan": {
"active": "Actif",
"addRepeatingPlan": "Ajouter un plan récurrent",
"arrivalTab": "Arrivée",
"day": "Jour",
"departureTab": "Départ",
"goal": "Objectif de charge",
"modalTitle": "Planification de la charge",
"none": "aucune",
@ -1222,8 +1220,6 @@
"strategySettings": "Paramètres de stratégie",
"time": "Heure",
"title": "Planification",
"titleMinSoc": "Charge min",
"titleTargetCharge": "Départ",
"unsavedChanges": "Il y a des modifications non sauvegardées. Appliquer ?",
"update": "Appliquer",
"weekdays": "Jours"
@ -1320,20 +1316,12 @@
"currents": "Courant de charge",
"default": "défaut",
"disclaimerHint": "Remarque :",
"limitSoc": {
"description": "Limite de charge à utiliser quand ce véhicule est connecté.",
"label": "Limite par défaut"
},
"maxCurrent": {
"label": "Courant max."
},
"minCurrent": {
"label": "Courant min."
},
"minSoc": {
"description": "Le véhicule est chargé en mode „Rapide” à {0} en mode solaire, puis avec le surplus solaire. Utile pour s'assurer d'un minimum d'autonomie, même pour les jours plus sombres.",
"label": "Charge min. %"
},
"onlyForSocBasedCharging": "Ces options sont disponibles uniquement pour les véhicules avec un niveau de charge connu.",
"phasesConfigured": {
"label": "Phases",

View file

@ -860,9 +860,7 @@
"chargingPlan": {
"active": "Aktivno",
"addRepeatingPlan": "Dodaj ponavljajući plan",
"arrivalTab": "Dolazak",
"day": "Dan",
"departureTab": "Odlazak",
"goal": "Cilj punjenja",
"modalTitle": "Plan punjenja",
"none": "nema, dodaj",
@ -886,8 +884,6 @@
"strategySettings": "Postavke strategije",
"time": "Vrijeme",
"title": "Plan",
"titleMinSoc": "Min. punjenje",
"titleTargetCharge": "Odlazak",
"unsavedChanges": "Postoje promjene koje nisu spremljene. Spremiti odmah?",
"update": "Primijeni",
"weekdays": "Dani"
@ -945,20 +941,12 @@
"currents": "Struja punjenja",
"default": "zadano",
"disclaimerHint": "Napomena:",
"limitSoc": {
"description": "Ograničenje punjenja koje se koristi kada je ovo vozilo spojeno.",
"label": "Zadano ograničenje"
},
"maxCurrent": {
"label": "Maksimalna struja punjenja"
},
"minCurrent": {
"label": "Minimalna struja punjenja"
},
"minSoc": {
"description": "Vozilo se puni „brzo” na {0} u solarnom modusu. Zatim se nastavlja puniti sa solarnim viškom. Korisno za osiguravanje minimalnog raspona čak i u danima s manje sunca.",
"label": "Min. % punjenja"
},
"onlyForSocBasedCharging": "Navedene opcije dostupne samo za vozila s poznatom razinom napunjenosti baterije.",
"phasesConfigured": {
"label": "Faze",

View file

@ -870,9 +870,7 @@
"chargingPlan": {
"active": "Aktív",
"addRepeatingPlan": "Ismétlődő terv hozzáadása",
"arrivalTab": "Érkezés",
"day": "Nap",
"departureTab": "Indulás",
"goal": "Töltési cél",
"modalTitle": "Töltési Tervezet",
"none": "nincs",
@ -896,8 +894,6 @@
"strategySettings": "Stratégiai beállítások",
"time": "Idő",
"title": "Terv",
"titleMinSoc": "Min töltés",
"titleTargetCharge": "Indulás",
"unsavedChanges": "Vannak nem mentett módosítások. Alkalmazza most?",
"update": "Alkalmazás",
"weekdays": "Napok"
@ -955,20 +951,12 @@
"currents": "Töltőáram",
"default": "alapértelmezett",
"disclaimerHint": "Megjegyzés:",
"limitSoc": {
"description": "Töltési korlát, amikor a jármű csatlakoztatva van.",
"label": "Alapértelmezett limit"
},
"maxCurrent": {
"label": "Max. Áram"
},
"minCurrent": {
"label": "Min. Áram"
},
"minSoc": {
"description": "A jármű gyorstöltve lesz {0}-ra szolár üzemmódban. Ezután folytatódik a töltés a napelemes többletenergiával. Hasznos egy minimum tartományt megadni a felhősebb napokra.",
"label": "Min. töltés %"
},
"onlyForSocBasedCharging": "Ezek a beállítások csak olyan járművekre elérhetőek, amiknek ismert a töltési szintje.",
"phasesConfigured": {
"label": "Fázis",

View file

@ -1098,9 +1098,7 @@
"chargingPlan": {
"active": "Attivo",
"addRepeatingPlan": "Aggiungi piano ricorrente",
"arrivalTab": "Arrivo",
"day": "Giorno",
"departureTab": "Partenza",
"goal": "Obiettivo di ricarica",
"modalTitle": "Piano di ricarica",
"none": "nessuno",
@ -1124,8 +1122,6 @@
"strategySettings": "Impostazioni strategia",
"time": "Ora",
"title": "Piano",
"titleMinSoc": "Carica minima",
"titleTargetCharge": "Partenza",
"unsavedChanges": "Ci sono modifiche non salvate. Vuoi salvarle adesso?",
"update": "Applica",
"weekdays": "Giorni"
@ -1191,20 +1187,12 @@
"currents": "Corrente di carica",
"default": "default",
"disclaimerHint": "Nota:",
"limitSoc": {
"description": "Limite di carica applicato quando il veicolo è connesso.",
"label": "Limite di default"
},
"maxCurrent": {
"label": "Corrente massima"
},
"minCurrent": {
"label": "Corrente min."
},
"minSoc": {
"description": "Per le emergenze. Il veicolo viene caricato 'velocemente' al {0} da tutto il solare disponibile, e poi continua con solo il surplus solare.",
"label": "Carica min. %"
},
"onlyForSocBasedCharging": "Questa opzione è solo per i veicoli dei quali si sa lo stato di carica.",
"phasesConfigured": {
"label": "Fasi",

View file

@ -1121,9 +1121,7 @@
"chargingPlan": {
"active": "アクティブ",
"addRepeatingPlan": "繰り返しプランを追加",
"arrivalTab": "到着",
"day": "日",
"departureTab": "出発",
"goal": "充電目標",
"modalTitle": "充電プラン",
"none": "なし",
@ -1147,8 +1145,6 @@
"strategySettings": "戦略設定",
"time": "時間",
"title": "プラン",
"titleMinSoc": "最小充電",
"titleTargetCharge": "出発",
"unsavedChanges": "保存されていない変更があります。今すぐ適用しますか?",
"update": "適用",
"weekdays": "日"
@ -1243,20 +1239,12 @@
"currents": "充電電流",
"default": "デフォルト",
"disclaimerHint": "注:",
"limitSoc": {
"description": "この車両が接続されたときに使用される充電制限。",
"label": "デフォルト制限"
},
"maxCurrent": {
"label": "最大電流"
},
"minCurrent": {
"label": "最小電流"
},
"minSoc": {
"description": "太陽光モード時、車両はまず „高速” で {0} まで充電されます。その後は太陽光の余剰電力で充電を継続します。日照の少ない日でも最低限の航続距離を確保するのに役立ちます。",
"label": "最小充電 %"
},
"onlyForSocBasedCharging": "これらのオプションは、バッテリー残量が取得できている車両でのみ利用可能です。",
"phasesConfigured": {
"label": "相数",

View file

@ -1119,9 +1119,7 @@
"chargingPlan": {
"active": "Aktiv",
"addRepeatingPlan": "Widderhuelte Planifikatioun derbäi fügen",
"arrivalTab": "Arrivée",
"day": "Dag",
"departureTab": "Depart",
"goal": "Luedzil",
"modalTitle": "Luedplanifikatioun",
"none": "keng",
@ -1145,8 +1143,6 @@
"strategySettings": "Strategie-Astellungen",
"time": "Zäit",
"title": "Planifikatioun",
"titleMinSoc": "Min. Oplueden",
"titleTargetCharge": "Depart",
"unsavedChanges": "Net gespäichert Ännerungen leie vir. Elo uwennen?",
"update": "Uwennen",
"weekdays": "Deeg"
@ -1241,20 +1237,12 @@
"currents": "Luedstroum",
"default": "Standard",
"disclaimerHint": "Notiz:",
"limitSoc": {
"description": "Luedzil dat benotzt gëtt, wann dëst Gefier verbonnen ass.",
"label": "Standard Luedzil"
},
"maxCurrent": {
"label": "Max. Stroum"
},
"minCurrent": {
"label": "Min. Stroum"
},
"minSoc": {
"description": "D'Gefier gëtt \"séier\" op {0} am Solarmodus opgelueden. Da geet et weider mat Solariwwerschëss. Dëst ass nëtzlech fir eng Minimum-Reechwäit och wärend méi däischter Deeg ze garantéieren.",
"label": "Min. oplueden %"
},
"onlyForSocBasedCharging": "Dës Optioune sinn nëmme fir Gefierer mat bekannte Luedstatioune verfügbar.",
"phasesConfigured": {
"label": "Phasen",

View file

@ -1198,9 +1198,7 @@
"chargingPlan": {
"active": "Aktyvus",
"addRepeatingPlan": "Pridėti pasikartojantį įkrovimo planą",
"arrivalTab": "Atvykimas",
"day": "Diena",
"departureTab": "Išvykimas",
"goal": "Įkrovimo tikslas",
"modalTitle": "Įkrovimo planas",
"none": "nėra",
@ -1224,8 +1222,6 @@
"strategySettings": "Strategijos pasirinkimai",
"time": "Laikas",
"title": "Planas",
"titleMinSoc": "Minimali įkrova",
"titleTargetCharge": "Išvykimas",
"unsavedChanges": "Yra neišsaugotų pakeitimų. Išsaugoti dabar?",
"update": "Patvirtinti",
"weekdays": "Dienos"
@ -1324,20 +1320,12 @@
"currents": "Įkrovimo Srovė",
"default": "standartiškai",
"disclaimerHint": "Pastaba:",
"limitSoc": {
"description": "Įkrovimo limitas, naudojamas, prijungus šį automobilį.",
"label": "Standartinis limitas"
},
"maxCurrent": {
"label": "Max. Srovė"
},
"minCurrent": {
"label": "Min. Srovė"
},
"minSoc": {
"description": "Automobilis įkraunamas „Greitai” iki {0} nustatyme „Saulė”, toliau įkraunamas tik saulės energijos pertekliumi. Padeda užtikrinti minimalią įkrovą dienomis, kai mažai saulės.",
"label": "Minimali įkrova %"
},
"onlyForSocBasedCharging": "Šie pasirinkimai galimi tik automobiliams su žinomu įkrovos lygiu.",
"phasesConfigured": {
"label": "Fazės",

View file

@ -1171,9 +1171,7 @@
"chargingPlan": {
"active": "Aktīvs",
"addRepeatingPlan": "Pievienot atkārtojošu plānu",
"arrivalTab": "Ierašanās",
"day": "Diena",
"departureTab": "Izbraukšana",
"goal": "Uzlādes mērķis",
"modalTitle": "Uzlādes plāns",
"none": "nav",
@ -1197,8 +1195,6 @@
"strategySettings": "Stratēģijas iestatījumi",
"time": "Laiks",
"title": "Plāns",
"titleMinSoc": "Min. uzlāde",
"titleTargetCharge": "Izbraukšana",
"unsavedChanges": "Ir nesaglabātas izmaiņas. Piemērot tagad?",
"update": "Piemērot",
"weekdays": "Dienas"
@ -1297,20 +1293,12 @@
"currents": "Uzlādes strāva",
"default": "noklusējuma",
"disclaimerHint": "Piezīme:",
"limitSoc": {
"description": "Uzlādes ierobežojums, kas tiek izmantots, kad šis transportlīdzeklis ir pievienots.",
"label": "Noklusējuma ierobežojums"
},
"maxCurrent": {
"label": "Maks. strāva"
},
"minCurrent": {
"label": "Min. strāva"
},
"minSoc": {
"description": "Transportlīdzeklis tiek „ātri” uzlādēts līdz {0} saules režīmā. Pēc tam turpina ar saules enerģijas pārpalikumu. Noderīgi, lai nodrošinātu minimālu nobraukumu pat tumšākām dienām.",
"label": "Min. uzlāde %"
},
"onlyForSocBasedCharging": "Šīs iespējas ir pieejamas tikai transportlīdzekļiem ar zināmu uzlādes līmeni.",
"phasesConfigured": {
"label": "Fāzes",

View file

@ -1177,9 +1177,7 @@
"chargingPlan": {
"active": "Actief",
"addRepeatingPlan": "Herhalend schema toevoegen",
"arrivalTab": "Aankomst",
"day": "Dag",
"departureTab": "Vertrek",
"goal": "Laaddoel",
"modalTitle": "Laad Plan",
"none": "geen",
@ -1203,8 +1201,6 @@
"strategySettings": "Strategie-instellingen",
"time": "Tijd",
"title": "Plan",
"titleMinSoc": "Min lading",
"titleTargetCharge": "Vertrek",
"unsavedChanges": "Wijzigingen nog niet opgeslagen. Nu toepassen?",
"update": "Toepassen",
"weekdays": "Dagen"
@ -1302,20 +1298,12 @@
"currents": "Laadstroom",
"default": "standaard",
"disclaimerHint": "Let op:",
"limitSoc": {
"description": "Standaard laadlimiet wanneer dit voertuig aangesloten wordt.",
"label": "Standaard limiet"
},
"maxCurrent": {
"label": "Max. Stroomsterkte"
},
"minCurrent": {
"label": "Min. Stroomsterkte"
},
"minSoc": {
"description": "Het voertuig wordt „snel” opgeladen tot {0} in zonne-modus. Het laden gaat daarna verder met zonneoverschot. Nuttig om een bepaald rijbereik te garanderen, ook gedurende periodes met weinig of geen zonnestroom.",
"label": "Min. batterij %"
},
"onlyForSocBasedCharging": "Deze instellingen zijn alleen beschikbaar bij voertuigen waar het batterijpercentage bekend is.",
"phasesConfigured": {
"label": "Fasen",

View file

@ -900,9 +900,7 @@
"chargingPlan": {
"active": "Aktiv",
"addRepeatingPlan": "Legg til gjentakende plan",
"arrivalTab": "Ankomst",
"day": "Dag",
"departureTab": "Avgang",
"goal": "Ladingsmål",
"modalTitle": "Ladeplan",
"none": "ingen",
@ -926,8 +924,6 @@
"strategySettings": "Strategiinnstillinger",
"time": "Tid",
"title": "Plan",
"titleMinSoc": "Min.-lading",
"titleTargetCharge": "Avgang",
"unsavedChanges": "Det er ubehandlede endringer. Vil du bruke dem nå?",
"update": "Søk",
"weekdays": "Dager"
@ -985,20 +981,12 @@
"currents": "Ladestrøm",
"default": "forvalg",
"disclaimerHint": "Merknad:",
"limitSoc": {
"description": "Ladegrense som brukes når dette kjøretøyet er tilkoblet.",
"label": "Standardgrense"
},
"maxCurrent": {
"label": "Maks. ladestrøm"
},
"minCurrent": {
"label": "Min. ladestrøm"
},
"minSoc": {
"description": "For nødstilfeller. Kjøretøyet blir „raskt” ladet til {0} fra all tilgjengelig solenergi, og fortsetter deretter med bare solenergioverskuddet.",
"label": "Min. lading%"
},
"onlyForSocBasedCharging": "Disse alternativene er kun tilgjengelige for kjøretøy med kjent ladenivå.",
"phasesConfigured": {
"label": "Faser",

View file

@ -1041,9 +1041,7 @@
"chargingPlan": {
"active": "Aktywny",
"addRepeatingPlan": "Dodaj plan powtarzalny",
"arrivalTab": "Przyjazd",
"day": "Dzień",
"departureTab": "Wyjazd",
"goal": "Cel ładowania",
"modalTitle": "Plan ładowania",
"none": "brak",
@ -1067,8 +1065,6 @@
"strategySettings": "Ustawienia strategii",
"time": "Czas",
"title": "Plan",
"titleMinSoc": "Minimalne ładowanie",
"titleTargetCharge": "Wyjazd",
"unsavedChanges": "Są niezapisane zmiany. Zastosować teraz?",
"update": "Zastosuj",
"weekdays": "Dni"
@ -1160,20 +1156,12 @@
"currents": "Prąd ładowania",
"default": "domyślny",
"disclaimerHint": "Uwaga:",
"limitSoc": {
"description": "Limit ładowania stosowany, gdy ten pojazd jest podłączony.",
"label": "Domyślny limit"
},
"maxCurrent": {
"label": "Maks. prąd"
},
"minCurrent": {
"label": "Min. Prąd"
},
"minSoc": {
"description": "Pojazd będzie „szybko” naładowany do {0} w trybie „Słońce”. Następnie kontynuuje tylko z nadwyżką fotowoltaiczną. Przydatne, aby zapewnić minimalny zasięg nawet w pochmurne dni.",
"label": "Minimalne naładowanie %"
},
"onlyForSocBasedCharging": "Opcje te są dostępne tylko dla pojazdów ze znanym poziomem naładowania.",
"phasesConfigured": {
"label": "Fazy",

View file

@ -1110,9 +1110,7 @@
"chargingPlan": {
"active": "Ativo",
"addRepeatingPlan": "Adicionar planos iguais",
"arrivalTab": "Fim",
"day": "Dia",
"departureTab": "Inicio",
"goal": "Carga pretendida",
"modalTitle": "Plano de carga",
"none": "nenhum",
@ -1136,8 +1134,6 @@
"strategySettings": "Definições da estratégia",
"time": "Hora",
"title": "Plano",
"titleMinSoc": "Carga mínima",
"titleTargetCharge": "Inicio",
"unsavedChanges": "Há alterações não salvas. Aplicar agora?",
"update": "Aplicar",
"weekdays": "Dias"
@ -1203,20 +1199,12 @@
"currents": "Corrente de carga",
"default": "predefinido",
"disclaimerHint": "Aviso:",
"limitSoc": {
"description": "limite de partilha que é usado quando este veículo está conectado.",
"label": "Limite padrão"
},
"maxCurrent": {
"label": "Corrente de carga máxima"
},
"minCurrent": {
"label": "Corrente de carga mínima"
},
"minSoc": {
"description": "O veículo é carregado “rápido” até {0} no modo solar. A seguir, continua com excedente solar. Útil para garantir um alcance mínimo mesmo em dias com pouco sol.",
"label": "Min. de carga %"
},
"onlyForSocBasedCharging": "Estas opções só estão disponíveis para veículos com nível de carregamento conhecido.",
"phasesConfigured": {
"label": "Fases elétricas",

View file

@ -1017,9 +1017,7 @@
"chargingPlan": {
"active": "Activ",
"addRepeatingPlan": "Adăugați plan repetitiv",
"arrivalTab": "Ajungere",
"day": "Zi",
"departureTab": "Plecare",
"goal": "Scopul incarcarii",
"modalTitle": "Plan de incarcare",
"none": "nimic",
@ -1043,8 +1041,6 @@
"strategySettings": "Setări strategie",
"time": "Timp",
"title": "Plan",
"titleMinSoc": "Incarcare minima",
"titleTargetCharge": "Plecare",
"unsavedChanges": "Există modificări nesalvate. Doriți să le aplicați acum?",
"update": "Aplica",
"weekdays": "Zile"
@ -1109,20 +1105,12 @@
"currents": "Curent încărcare",
"default": "default",
"disclaimerHint": "Nota:",
"limitSoc": {
"description": "Limita de incarcare care e folosita cand e conectat a aceasta masina.",
"label": "Limita default"
},
"maxCurrent": {
"label": "Amperaj maxim"
},
"minCurrent": {
"label": "Amperaj minim"
},
"minSoc": {
"description": "Vehiculul primește încărcare rapida {0} din toata energia solara disponibilă, apoi continua încărcarea cu surplusul solar.",
"label": "Min. încărcare %"
},
"onlyForSocBasedCharging": "Aceste optiuni sunt active doar pentru masini carora le cunoaste gradul de incarcare.",
"phasesConfigured": {
"label": "Faze",

View file

@ -850,9 +850,7 @@
"chargingPlan": {
"active": "Активный",
"addRepeatingPlan": "Добавить повторяющийся план",
"arrivalTab": "Прибытие",
"day": "День",
"departureTab": "Отправление",
"goal": "Цель зарядки",
"modalTitle": "План тарификации",
"none": "нет",
@ -876,8 +874,6 @@
"strategySettings": "Настройки стратегии",
"time": "Время",
"title": "План",
"titleMinSoc": "Минимальная плата",
"titleTargetCharge": "Отправление",
"unsavedChanges": "Есть несохраненные изменения. Применить сейчас?",
"update": "Применить",
"weekdays": "Дни"
@ -935,20 +931,12 @@
"currents": "Ток зарядки",
"default": "по умолчанию",
"disclaimerHint": "Заметка:",
"limitSoc": {
"description": "Предел зарядки, который используется при подключении этого транспортного средства.",
"label": "Предельное значение по умолчанию"
},
"maxCurrent": {
"label": "Макс. ток"
},
"minCurrent": {
"label": "Мин. ток"
},
"minSoc": {
"description": "Автомобиль «быстро» заряжается до {0} в солнечном режиме. Затем продолжает работу за счет избытка солнечной энергии. Полезно для обеспечения минимального запаса хода даже в пасмурные дни.",
"label": "Мин. заряд %"
},
"onlyForSocBasedCharging": "Эти опции доступны только для автомобилей с известным уровнем заряда.",
"phasesConfigured": {
"label": "Фазы",

View file

@ -876,9 +876,7 @@
"chargingPlan": {
"active": "Aktivno",
"addRepeatingPlan": "Dodaj ponavljajoči načrt",
"arrivalTab": "Prihod",
"day": "Dan",
"departureTab": "Odhod",
"goal": "Cilj polnjenja",
"modalTitle": "Načrt polnjenja",
"none": "brez",
@ -907,8 +905,6 @@
"strategySettings": "Nastavitve strategije",
"time": "Čas",
"title": "Načrt",
"titleMinSoc": "Min. polnjenje",
"titleTargetCharge": "Odhod",
"unsavedChanges": "Obstajajo neshranjene spremembe. Želite uporabiti zdaj?",
"update": "Uporabi",
"weekdays": "Dnevi"
@ -966,20 +962,12 @@
"currents": "Polnilni tok",
"default": "privzeto",
"disclaimerHint": "Opomba:",
"limitSoc": {
"description": "Omejitev polnjenja, ki se uporablja, ko je to vozilo povezano.",
"label": "Privzeta omejitev"
},
"maxCurrent": {
"label": "Maks. tok"
},
"minCurrent": {
"label": "Min. tok"
},
"minSoc": {
"description": "Za nujne primere. Vozilo se »hitro« napolni na {0} od vse razpoložljive solarne energije, nato pa nadaljuje samo s solarnim presežkom.",
"label": "Min. polnjenje %"
},
"onlyForSocBasedCharging": "Te možnosti so na voljo samo za vozila z znano stopnjo napolnjenosti.",
"phasesConfigured": {
"label": "Faze",

View file

@ -1169,9 +1169,7 @@
"chargingPlan": {
"active": "Aktiv",
"addRepeatingPlan": "Lägg till en återkommande laddplan",
"arrivalTab": "Ankomst",
"day": "Dag",
"departureTab": "Avfärd",
"goal": "Laddmål",
"modalTitle": "Laddplan",
"none": "ingen",
@ -1195,8 +1193,6 @@
"strategySettings": "Strategiinställningar",
"time": "Tid",
"title": "Plan",
"titleMinSoc": "Min. laddning",
"titleTargetCharge": "Avfärd",
"unsavedChanges": "Det finns ej sparade ändringar. Spara nu?",
"update": "Verkställ",
"weekdays": "Dagar"
@ -1295,20 +1291,12 @@
"currents": "Laddström",
"default": "förvald",
"disclaimerHint": "Anmärkning:",
"limitSoc": {
"description": "Laddbegränsning när detta fordon är anslutet.",
"label": "Förvald laddgräns"
},
"maxCurrent": {
"label": "Max. ström"
},
"minCurrent": {
"label": "Min. ström"
},
"minSoc": {
"description": "Fordonet laddas i sol-läge „snabbt” till {0} och sedan med endast solel. Användbart för att alltid ha en minsta räckvidd tillgänglig .",
"label": "Min. laddning %"
},
"onlyForSocBasedCharging": "Dessa inställningar är endast tillgängliga för fordon med känd laddnivå.",
"phasesConfigured": {
"label": "Faser",

View file

@ -935,9 +935,7 @@
"chargingPlan": {
"active": "செயலில்",
"addRepeatingPlan": "மறுநிகழ்வு திட்டத்தைச் சேர்",
"arrivalTab": "வருகை",
"day": "நாள்",
"departureTab": "புறப்படுதல்",
"goal": "மின்சேர்வி இலக்கு",
"modalTitle": "மின்சேர்வி திட்டம்",
"none": "எதுவுமில்லை",
@ -961,8 +959,6 @@
"strategySettings": "மூலோபாய அமைப்புகள்",
"time": "நேரம்",
"title": "திட்டம்",
"titleMinSoc": "\"குறை. மின்னூட்டு\"",
"titleTargetCharge": "புறப்படுதல்",
"unsavedChanges": "There அரே unsaved changes. இடு now?",
"update": "இடு",
"weekdays": "நாட்கள்"
@ -1027,20 +1023,12 @@
"currents": "மின்சேர்வி மின்னோட்டம்",
"default": "இயல்புநிலை",
"disclaimerHint": "குறிப்பு:",
"limitSoc": {
"description": "இந்த வாகனம் இணைக்கப்படும்போது பயன்படுத்தப்படும் மின்சேர்வி வரம்பு.",
"label": "இயல்புநிலை வரம்பு"
},
"maxCurrent": {
"label": "அதிக. மின்னோட்டம்"
},
"minCurrent": {
"label": "குறை. மின்னோட்டம்"
},
"minSoc": {
"description": "The vehicle gets „fast” charged பெறுநர் {0} in solar mode. Then continues with solar surplus. Useful பெறுநர் ensure a சிறுமம் வீச்சு இரட்டை க்கு darker days.",
"label": "Min. மின்னூட்டு %"
},
"onlyForSocBasedCharging": "These விருப்பங்கள் அரே only available க்கு vehicles with known charging level.",
"phasesConfigured": {
"label": "கட்டங்கள்",

View file

@ -1143,9 +1143,7 @@
"chargingPlan": {
"active": "Aktif",
"addRepeatingPlan": "“Tekrar eden plan ekle”",
"arrivalTab": "Varış",
"day": "Gün",
"departureTab": "Ayrılış",
"goal": "Doldurma hedefi",
"modalTitle": "Doldurma Planı",
"none": "yok",
@ -1169,8 +1167,6 @@
"strategySettings": "Strateji ayarları",
"time": "Zaman",
"title": "Plan",
"titleMinSoc": "Asgari doldurma",
"titleTargetCharge": "Ayrılış",
"unsavedChanges": "Kaydedilmemiş değişiklikler var. Şimdi uygulansın mı?",
"update": "Uygula",
"weekdays": "Günler"
@ -1265,20 +1261,12 @@
"currents": "Doldurma Akımı",
"default": "varsayılan",
"disclaimerHint": "Not:",
"limitSoc": {
"description": "Araç bağlandığında kullanılan dolum sınırı.",
"label": "Varsayılan dolum sınır"
},
"maxCurrent": {
"label": "Azami Akım"
},
"minCurrent": {
"label": "Asgari Akım"
},
"minSoc": {
"description": "Araç, güneş enerjisi modunda {0} seviyesine hızlı doldurulur. Ardından güneş enerjisi fazlalığıyla devam eder. Karanlık havalarda dahi asgari bir menzil sağlamak için kullanışlıdır.",
"label": "Asgari dolum oranı"
},
"onlyForSocBasedCharging": "Bu seçenekler, sadece doluluk seviyesi bilinen araçlar için açık.",
"phasesConfigured": {
"label": "Fazlar",

View file

@ -863,9 +863,7 @@
"chargingPlan": {
"active": "Активний",
"addRepeatingPlan": "Додати повторюваний план",
"arrivalTab": "Прибуття",
"day": "День",
"departureTab": "Від'їзд",
"goal": "Зарядка гол",
"modalTitle": "Тарифний план",
"none": "немає",
@ -889,8 +887,6 @@
"strategySettings": "Налаштування стратегії",
"time": "Час",
"title": "План",
"titleMinSoc": "Мінімальна плата",
"titleTargetCharge": "Від'їзд",
"unsavedChanges": "Є незбережені зміни. Застосувати зараз?",
"update": "Застосувати",
"weekdays": "Днів"
@ -948,20 +944,12 @@
"currents": "Струм Зарядки",
"default": "за замовчуванням",
"disclaimerHint": "Примітка:",
"limitSoc": {
"description": "Ліміт заряджання, який використовується, коли цей автомобіль під’єднано.",
"label": "Ліміт за замовчуванням"
},
"maxCurrent": {
"label": "Макс. струм"
},
"minCurrent": {
"label": "Мін. струм"
},
"minSoc": {
"description": "Для надзвичайних ситуацій. Транспортний засіб «швидко» заряджається до {0} від усіх доступних сонячних батарей, а потім продовжує працювати лише з сонячним надлишком.",
"label": "Мін. заряд %"
},
"onlyForSocBasedCharging": "Ці параметри доступні лише для автомобілів із відомим рівнем заряду.",
"phasesConfigured": {
"label": "Фази",

View file

@ -614,9 +614,7 @@
"chargingPlan": {
"active": "启用",
"addRepeatingPlan": "添加重复计划",
"arrivalTab": "抵达",
"day": "日期",
"departureTab": "离开",
"goal": "充电目标",
"modalTitle": "充电计划",
"none": "无",
@ -633,8 +631,6 @@
"selectAll": "全选",
"time": "时间",
"title": "计划",
"titleMinSoc": "最小充电量",
"titleTargetCharge": "出发时间",
"unsavedChanges": "有未保存的更改。是否立即应用?",
"update": "应用",
"weekdays": "天数"
@ -688,20 +684,12 @@
"currents": "充电电流",
"default": "默认",
"disclaimerHint": "注意:",
"limitSoc": {
"description": "连接此车辆时使用的充电上限。",
"label": "默认上限"
},
"maxCurrent": {
"label": "最大电流"
},
"minCurrent": {
"label": "最小电流"
},
"minSoc": {
"description": "在太阳能模式下,车辆将“快速”充电至 {0},然后继续使用太阳能余电充电。这有助于确保即使在光照不足的日子也能达到最低续航里程。",
"label": "最小电量 %"
},
"onlyForSocBasedCharging": "这些选项仅适用于已知充电量的车辆。",
"phasesConfigured": {
"label": "相数",

View file

@ -181,11 +181,14 @@ test.describe("vehicles", async () => {
/active/
);
await lp.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = page.getByTestId("charging-plan-modal");
const moreTab = page.getByTestId("tab-more");
await moreTab.click();
await moreTab.getByRole("button", { name: "Vehicles" }).click();
const modal = page.getByTestId("vehicle-settings-modal");
await expectModalVisible(modal);
await modal.getByRole("link", { name: "Arrival" }).click();
await expect(modal.getByRole("combobox", { name: "Default mode" })).toHaveValue("now");
await expect(
modal.getByRole("group", { name: "Grey Car" }).getByRole("combobox", { name: "Default mode" })
).toHaveValue("now");
});
test("save and restore rfid identifiers", async ({ page }) => {

View file

@ -1,5 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
import { expectModalVisible } from "./utils";
test.use({ baseURL: baseUrl() });
@ -20,16 +21,18 @@ test.describe("vehicle startup error (using failing Tesla API)", async () => {
await expect(page.getByTestId("vehicle-name")).toHaveText("Broken Tesla");
await expect(page.getByTestId("vehicle-not-reachable-icon")).toBeVisible();
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = page.getByTestId("charging-plan-modal");
await modal.getByRole("link", { name: "Arrival" }).click();
await expect(modal.getByRole("combobox", { name: "Min. charge %" })).toBeEnabled();
const moreTab = page.getByTestId("tab-more");
await moreTab.click();
await moreTab.getByRole("button", { name: "Vehicles" }).click();
const modal = page.getByTestId("vehicle-settings-modal");
await expectModalVisible(modal);
await expect(modal.getByRole("combobox", { name: "Minimum charge" })).toBeEnabled();
await expect(modal.getByRole("combobox", { name: "Default limit" })).toBeEnabled();
});
test("guest vehicle: normal title and no icon", async ({ page }) => {
// switch to offline vehicle
await page.getByTestId("change-vehicle").locator("select").selectOption("Guest vehicle");
await page.getByRole("combobox", { name: "Change vehicle" }).selectOption("Guest vehicle");
await expect(page.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
await expect(page.getByTestId("vehicle-not-reachable-icon")).not.toBeVisible();

View file

@ -1,4 +1,4 @@
import { test, expect, type Page } from "@playwright/test";
import { test, expect, type Locator, type Page } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden } from "./utils";
import {
@ -32,45 +32,109 @@ test.afterEach(async () => {
await stop();
});
async function switchVehicle(page: Page, title: string) {
await page.getByRole("combobox", { name: "Change vehicle" }).selectOption(title);
await expect(page.getByTestId("vehicle-name")).toHaveText(title);
}
async function openVehicleSettings(page: Page) {
const moreTab = page.getByTestId("tab-more");
await moreTab.click();
await moreTab.getByRole("button", { name: "Vehicles" }).click();
const modal = page.getByTestId("vehicle-settings-modal");
await expectModalVisible(modal);
return modal;
}
function vehicleRow(modal: Locator, title: string) {
return modal.getByRole("group", { name: title });
}
async function closeModal(page: Page) {
const modal = page.getByTestId("vehicle-settings-modal");
await modal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(modal);
}
test.describe("overview", async () => {
test("all configured vehicles listed", async ({ page }) => {
await page.goto("/");
const modal = await openVehicleSettings(page);
await expect(modal.getByRole("group")).toHaveCount(2);
await expect(vehicleRow(modal, "blauer e-Golf")).toBeVisible();
await expect(vehicleRow(modal, "grüner Honda e")).toBeVisible();
await expect(modal).not.toContainText("Guest vehicle");
});
test("connected indicator only on connected vehicle", async ({ page }) => {
await page.goto("/");
const modal = await openVehicleSettings(page);
await expect(
vehicleRow(modal, "blauer e-Golf").getByText("connected to Carport")
).toBeVisible();
await expect(vehicleRow(modal, "grüner Honda e").getByText("not connected")).toBeVisible();
});
test("per-vehicle values are independent", async ({ page }) => {
await page.goto("/");
const modal = await openVehicleSettings(page);
const golfMode = vehicleRow(modal, "blauer e-Golf").getByRole("combobox", {
name: "Default mode",
});
const hondaMode = vehicleRow(modal, "grüner Honda e").getByRole("combobox", {
name: "Default mode",
});
await hondaMode.selectOption("Fast");
await expect(hondaMode).toHaveValue("now");
await expect(golfMode).toHaveValue("");
});
});
test.describe("minSoc", async () => {
test("apply and restart", async ({ page }) => {
await page.goto("/");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = page.getByTestId("charging-plan-modal");
await expectModalVisible(modal);
await modal.getByRole("link", { name: "Arrival" }).click();
await expect(modal).toContainText("charged to x in solar mode");
await modal.getByRole("combobox", { name: "Min. charge %" }).selectOption("20%");
await expect(modal).toContainText("charged to 20% in solar mode");
await modal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(modal);
const modal = await openVehicleSettings(page);
const minSoc = vehicleRow(modal, "blauer e-Golf").getByRole("combobox", {
name: "Minimum charge",
});
await minSoc.selectOption("20");
await expect(minSoc).toHaveValue("20");
await closeModal(page);
await page.waitForLoadState("networkidle");
await restart(simulatorConfig());
await page.reload();
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByRole("link", { name: "Arrival" }).click();
await expect(page.getByText("charged to 20% in solar mode")).toBeVisible();
const modalAfterRestart = await openVehicleSettings(page);
await expect(
vehicleRow(modalAfterRestart, "blauer e-Golf").getByRole("combobox", {
name: "Minimum charge",
})
).toHaveValue("20");
});
test("show minsoc indicator when minsoc is active", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("charging-plan")).toContainText("Plan");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByRole("link", { name: "Arrival" }).click();
await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("50%");
await page.getByRole("button", { name: "Close" }).click();
const modal = await openVehicleSettings(page);
const minSoc = vehicleRow(modal, "blauer e-Golf").getByRole("combobox", {
name: "Minimum charge",
});
await minSoc.selectOption("50");
await closeModal(page);
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 expectModalVisible(modal);
await minSoc.selectOption("0");
await closeModal(page);
await expect(page.getByTestId("vehicle-status-minsoc")).not.toBeVisible();
});
@ -80,26 +144,25 @@ test.describe("limitSoc", async () => {
test("apply and restart", async ({ page }) => {
await page.goto("/");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = page.getByTestId("charging-plan-modal");
await expectModalVisible(modal);
await modal.getByRole("link", { name: "Arrival" }).click();
await modal.getByRole("combobox", { name: "Default limit" }).selectOption("80%");
await modal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(modal);
const modal = await openVehicleSettings(page);
await vehicleRow(modal, "blauer e-Golf")
.getByRole("combobox", { name: "Default limit" })
.selectOption("80");
await closeModal(page);
await expect(page.getByTestId("limit-soc-value")).toContainText("80%");
await page.waitForLoadState("networkidle");
await page.waitForTimeout(500); // bad practice but may help here :/
await restart(simulatorConfig());
await page.reload();
await expect(page.getByTestId("limit-soc-value")).toContainText("80%");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByRole("link", { name: "Arrival" }).click();
await expect(page.getByRole("combobox", { name: "Default limit" })).toHaveValue("80");
const modalAfterRestart = await openVehicleSettings(page);
await expect(
vehicleRow(modalAfterRestart, "blauer e-Golf").getByRole("combobox", {
name: "Default limit",
})
).toHaveValue("80");
});
});
@ -107,69 +170,64 @@ test.describe("minSoc and limitSoc", async () => {
test("hidden for offline vehicles", async ({ page }) => {
await page.goto("/");
// switch to offline vehicle
await page.getByTestId("change-vehicle").locator("select").selectOption("grüner Honda e");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByRole("link", { name: "Arrival" }).click();
await expect(page.getByRole("combobox", { name: "Min. charge %" })).not.toBeVisible();
await expect(page.getByRole("combobox", { name: "Default limit" })).not.toBeVisible();
});
test("no tabs for guest vehicles", async ({ page }) => {
await page.goto("/");
// switch to guest vehicle (no known vehicle)
await page.getByTestId("change-vehicle").locator("select").selectOption("Guest vehicle");
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = page.getByTestId("charging-plan-modal");
await expectModalVisible(modal);
await expect(modal.getByRole("link", { name: "Departure" })).toHaveCount(0);
await expect(modal.getByRole("link", { name: "Arrival" })).toHaveCount(0);
const modal = await openVehicleSettings(page);
const honda = vehicleRow(modal, "grüner Honda e");
await expect(honda.getByRole("combobox", { name: "Default mode" })).toBeVisible();
await expect(honda.getByRole("combobox", { name: "Minimum charge" })).not.toBeVisible();
await expect(honda.getByRole("combobox", { name: "Default limit" })).not.toBeVisible();
});
});
test.describe("default mode", async () => {
async function setDefaultMode(page: Page, option: string) {
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
const modal = page.getByTestId("charging-plan-modal");
await expectModalVisible(modal);
await modal.getByRole("link", { name: "Arrival" }).click();
await modal.getByRole("combobox", { name: "Default mode" }).selectOption(option);
await modal.getByRole("button", { name: "Close" }).click();
await expectModalHidden(modal);
async function setDefaultMode(page: Page, title: string, option: string) {
const modal = await openVehicleSettings(page);
await vehicleRow(modal, title)
.getByRole("combobox", { name: "Default mode" })
.selectOption(option);
await closeModal(page);
}
test("apply and restart", async ({ page }) => {
await page.goto("/");
await setDefaultMode(page, "Fast");
await setDefaultMode(page, "blauer e-Golf", "Fast");
await page.waitForLoadState("networkidle");
await restart(simulatorConfig());
await page.reload();
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
await page.getByRole("link", { name: "Arrival" }).click();
await expect(page.getByRole("combobox", { name: "Default mode" })).toHaveValue("now");
const modal = await openVehicleSettings(page);
await expect(
vehicleRow(modal, "blauer e-Golf").getByRole("combobox", { name: "Default mode" })
).toHaveValue("now");
});
test("applied on vehicle switch", async ({ page }) => {
await page.goto("/");
const mode = page.getByTestId("loadpoint").first().getByTestId("mode");
const changeVehicle = page.getByTestId("change-vehicle").locator("select");
// per-vehicle default modes
await setDefaultMode(page, "Solar");
await changeVehicle.selectOption("grüner Honda e");
await setDefaultMode(page, "Fast");
await setDefaultMode(page, "blauer e-Golf", "Solar");
await setDefaultMode(page, "grüner Honda e", "Fast");
// switching vehicles applies the vehicle's default mode
await changeVehicle.selectOption("blauer e-Golf");
await expect(mode.getByRole("button", { name: "Solar", exact: true })).toHaveClass(/active/);
await changeVehicle.selectOption("grüner Honda e");
await switchVehicle(page, "grüner Honda e");
await expect(mode.getByRole("button", { name: "Fast" })).toHaveClass(/active/);
await switchVehicle(page, "blauer e-Golf");
await expect(mode.getByRole("button", { name: "Solar", exact: true })).toHaveClass(/active/);
});
});
test.describe("menu entry", async () => {
test("hidden when no vehicles are configured", async ({ page }) => {
await stop();
await start("basics.evcc.yaml");
await page.goto("/");
const moreTab = page.getByTestId("tab-more");
await moreTab.click();
await expect(moreTab.getByRole("button", { name: "User Interface" })).toBeVisible();
await expect(moreTab.getByRole("button", { name: "Vehicles" })).not.toBeVisible();
});
});