Config UI: release and final touches (#25762)

This commit is contained in:
Michael Geers 2025-12-20 23:52:07 +01:00 • committed by GitHub
parent 4d09af22f7
commit f202d05474
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 438 additions and 590 deletions

View file

@ -1,7 +1,12 @@
<template> <template>
<div <div
class="root round-box" class="root"
:class="{ 'round-box--error': error, 'round-box--warning': warning }" :class="{
'round-box': !unconfigured,
'round-box--error': error,
'round-box--warning': warning,
'root--unconfigured': unconfigured,
}"
> >
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<div class="icon me-2"> <div class="icon me-2">
@ -16,7 +21,7 @@
<button <button
ref="tooltip" ref="tooltip"
type="button" type="button"
class="btn btn-sm btn-outline-secondary position-relative border-0 p-2" class="btn btn-sm btn-outline-secondary position-relative border-0 p-2 edit-button"
:class="{ 'opacity-25': !editable, invisible: noEditButton }" :class="{ 'opacity-25': !editable, invisible: noEditButton }"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-html="true" data-bs-html="true"
@ -45,6 +50,7 @@ export default {
title: String, title: String,
editable: Boolean, editable: Boolean,
error: Boolean, error: Boolean,
unconfigured: Boolean,
warning: Boolean, warning: Boolean,
noEditButton: Boolean, noEditButton: Boolean,
}, },
@ -101,9 +107,26 @@ export default {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
min-height: 8rem; min-height: 8rem;
} }
.root--unconfigured {
background: none;
border: 1px solid var(--evcc-gray-50);
transition: border-color var(--evcc-transition-fast) linear;
}
.root--unconfigured:hover {
border-color: var(--evcc-default-text);
}
.root--unconfigured :deep(.value),
.root--unconfigured :deep(.label) {
color: var(--evcc-gray) !important;
font-weight: normal !important;
}
.icon:empty { .icon:empty {
display: none; display: none;
} }
.edit-button {
/* transparent button, right align icon */
margin-right: -0.5rem;
}
.divide { .divide {
margin-left: -1.5rem; margin-left: -1.5rem;
margin-right: -1.5rem; margin-right: -1.5rem;

View file

@ -1,18 +0,0 @@
<template>
<div class="alert alert-warning my-4 pb-0" role="alert" data-testid="experimental-banner">
<p>
<strong>Experimental! 🧪</strong>
The UI configuration is still in development. Found an issue? Please
<a href="https://github.com/evcc-io/evcc/issues" target="_blank">report it on GitHub</a
>. Need to reset? Delete <code>evcc.db</code> or run <code>evcc migrate --reset</code>.
Note: Disabling experimental features only hides the UI features. Your configuration
stays untouched.
</p>
</div>
</template>
<script>
export default {
name: "ExperimentalBanner",
};
</script>

View file

@ -0,0 +1,47 @@
<template>
<GenericModal
id="experimentalModal"
:title="$t('config.experimental.title')"
data-testid="experimental-modal"
>
<p>{{ $t("config.experimental.description") }}</p>
<div class="form-check form-switch my-3">
<input
id="experimentalEnabled"
v-model="hiddenFeatures"
class="form-check-input"
type="checkbox"
role="switch"
/>
<div class="form-check-label">
<label for="experimentalEnabled">
{{ $t("settings.hiddenFeatures.value") }} 🧪
</label>
</div>
</div>
<div class="small text-muted">
{{ $t("settings.deviceInfo") }}
</div>
</GenericModal>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import GenericModal from "../Helper/GenericModal.vue";
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags.ts";
export default defineComponent({
name: "ExperimentalModal",
components: { GenericModal },
data() {
return {
hiddenFeatures: getHiddenFeatures(),
};
},
watch: {
hiddenFeatures(value) {
setHiddenFeatures(value);
},
},
});
</script>

View file

@ -26,17 +26,15 @@
test-id="generalconfig-experimental" test-id="generalconfig-experimental"
:label="$t('config.general.experimental')" :label="$t('config.general.experimental')"
:text="$t(`config.general.${hiddenFeatures ? 'on' : 'off'}`)" :text="$t(`config.general.${hiddenFeatures ? 'on' : 'off'}`)"
modal-id="globalSettingsModal" modal-id="experimentalModal"
/> />
<GeneralConfigEntry <GeneralConfigEntry
v-if="$hiddenFeatures()"
test-id="generalconfig-sponsoring" test-id="generalconfig-sponsoring"
:label="$t('config.sponsor.title')" :label="$t('config.sponsor.title')"
:text="sponsorStatus.title" :text="sponsorStatus.title"
:text-class="sponsorStatus.textClass" :text-class="sponsorStatus.textClass"
modal-id="sponsorModal" modal-id="sponsorModal"
experimental
> >
<template #text-prefix> <template #text-prefix>
<span <span
@ -48,21 +46,17 @@
</GeneralConfigEntry> </GeneralConfigEntry>
<GeneralConfigEntry <GeneralConfigEntry
v-if="$hiddenFeatures()"
test-id="generalconfig-network" test-id="generalconfig-network"
:label="$t('config.network.title')" :label="$t('config.network.title')"
:text="networkStatus" :text="networkStatus"
modal-id="networkModal" modal-id="networkModal"
experimental
/> />
<GeneralConfigEntry <GeneralConfigEntry
v-if="$hiddenFeatures()"
test-id="generalconfig-control" test-id="generalconfig-control"
:label="$t('config.control.title')" :label="$t('config.control.title')"
:text="controlStatus" :text="controlStatus"
modal-id="controlModal" modal-id="controlModal"
experimental
/> />
<TitleModal ref="titleModal" @changed="load" /> <TitleModal ref="titleModal" @changed="load" />
</div> </div>

View file

@ -3,9 +3,7 @@
class="d-flex justify-content-between flex-wrap align-items-center gap-2" class="d-flex justify-content-between flex-wrap align-items-center gap-2"
:data-testid="testId" :data-testid="testId"
> >
<strong class="text-truncate d-flex align-items-center" <strong class="text-truncate d-flex align-items-center">{{ label }}</strong>
>{{ label }}{{ experimental ? " 🧪" : "" }}</strong
>
<div class="d-flex align-items-center text-truncate"> <div class="d-flex align-items-center text-truncate">
<div <div
class="text-truncate align-items-center flex-grow-1 flex-shrink-1 text-end" class="text-truncate align-items-center flex-grow-1 flex-shrink-1 text-end"
@ -41,7 +39,6 @@ export default {
text: { type: String, default: "---" }, text: { type: String, default: "---" },
textClass: { type: String, default: "" }, textClass: { type: String, default: "" },
modalId: { type: String, required: true }, modalId: { type: String, required: true },
experimental: { type: Boolean, default: false },
}, },
emits: ["edit-clicked"], emits: ["edit-clicked"],
methods: { methods: {

View file

@ -1,75 +1,15 @@
<template> <template>
<div class="alert alert-success" data-testid="welcome-banner"> <div class="alert alert-success" data-testid="welcome-banner">
<h5 class="alert-heading">Welcome to evcc!</h5> <h5 class="alert-heading">{{ $t("config.main.welcomeBannerTitle") }}</h5>
<p class="mb-4">Ready to get started? Choose how you'd like to configure your devices:</p> <Markdown :markdown="$t('config.main.welcomeBannerText')" class="mb-0" />
<div class="row g-5 mb-4">
<div class="col-12 col-md-6">
<strong>Configuration File</strong>
<p>The traditional and <u>recommended</u> approach.</p>
<p>
Create an evcc.yaml file with all your device settings. Once set up, you can
make day-to-day adjustments right here in the UI.
</p>
<p>Need help getting started?</p>
<a
href="https://docs.evcc.io/en/docs/installation/configuration"
class="btn btn-outline-primary btn-sm"
tabindex="0"
target="_blank"
>Configuration Guide</a
>
</div>
<div class="col-12 col-md-6">
<div>
<strong>UI Configuration 🧪</strong>
<p>The new but still <u>experimental</u> approach.</p>
<p>
Configure everything directly in this interface. While functional, we're
still working on
<a target="_blank" href="https://github.com/evcc-io/evcc/issues/6029"
>improvements</a
>
and fixing
<a
target="_blank"
href="https://github.com/evcc-io/evcc/issues?q=is%3Aissue%20state%3Aopen%20label%3Aexperimental"
>known issues</a
>.
</p>
<p>Want to try it out?</p>
<button class="btn btn-sm" :class="buttonClass" tabindex="0" @click="toggle">
{{ buttonLabel }}
</button>
</div>
</div>
</div>
<p class="small mb-0">
<strong>Note:</strong> This message disappears once you configure your first charging
point (loadpoint).
</p>
</div> </div>
</template> </template>
<script> <script>
import { setHiddenFeatures, getHiddenFeatures } from "@/featureflags.ts"; import Markdown from "./Markdown.vue";
export default { export default {
name: "WelcomeBanner", name: "WelcomeBanner",
computed: { components: { Markdown },
buttonClass() {
return this.hiddenFeatures() ? "btn-outline-secondary" : "btn-outline-primary";
},
buttonLabel() {
return this.hiddenFeatures() ? "✔︎ enabled" : "Enable Experimental Features";
},
},
methods: {
toggle() {
setHiddenFeatures(!this.hiddenFeatures());
},
hiddenFeatures() {
return getHiddenFeatures();
},
},
}; };
</script> </script>

View file

@ -74,22 +74,6 @@
{{ $t("settings.fullscreen.enter") }} {{ $t("settings.fullscreen.enter") }}
</button> </button>
</FormRow> </FormRow>
<FormRow id="hiddenFeaturesEnabled" :label="`${$t('settings.hiddenFeatures.label')} 🧪`">
<div class="form-check form-switch my-1">
<input
id="hiddenFeaturesEnabled"
v-model="hiddenFeatures"
class="form-check-input"
type="checkbox"
role="switch"
/>
<div class="form-check-label">
<label for="hiddenFeaturesEnabled">
{{ $t("settings.hiddenFeatures.value") }}
</label>
</div>
</div>
</FormRow>
<div class="small text-muted mb-3"> <div class="small text-muted mb-3">
{{ $t("settings.deviceInfo") }} {{ $t("settings.deviceInfo") }}
</div> </div>
@ -108,7 +92,6 @@ import {
} from "@/i18n.ts"; } from "@/i18n.ts";
import { getThemePreference, setThemePreference } from "@/theme.ts"; import { getThemePreference, setThemePreference } from "@/theme.ts";
import { getUnits, setUnits, is12hFormat, set12hFormat } from "@/units"; import { getUnits, setUnits, is12hFormat, set12hFormat } from "@/units";
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags.ts";
import { isApp } from "@/utils/native"; import { isApp } from "@/utils/native";
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import { LENGTH_UNIT, THEME, type UiLoadpoint } from "@/types/evcc"; import { LENGTH_UNIT, THEME, type UiLoadpoint } from "@/types/evcc";
@ -128,7 +111,6 @@ export default defineComponent({
language: getLocalePreference() || "", language: getLocalePreference() || "",
unit: getUnits(), unit: getUnits(),
timeFormat: is12hFormat() ? TIME_12H : TIME_24H, timeFormat: is12hFormat() ? TIME_12H : TIME_24H,
hiddenFeatures: getHiddenFeatures(),
fullscreenActive: false, fullscreenActive: false,
THEMES: Object.values(THEME), THEMES: Object.values(THEME),
UNITS: Object.values(LENGTH_UNIT), UNITS: Object.values(LENGTH_UNIT),
@ -162,9 +144,6 @@ export default defineComponent({
theme(value) { theme(value) {
setThemePreference(value); setThemePreference(value);
}, },
hiddenFeatures(value) {
setHiddenFeatures(value);
},
language(value) { language(value) {
const i18n = this.$root?.$i18n; const i18n = this.$root?.$i18n;
if (!i18n) return; if (!i18n) return;

View file

@ -296,7 +296,7 @@ export default defineComponent({
return this.pvConfigured || this.gridConfigured; return this.pvConfigured || this.gridConfigured;
}, },
batteryBoostAvailable() { batteryBoostAvailable() {
return this.batteryConfigured && this.$hiddenFeatures(); return this.batteryConfigured;
}, },
batteryBoostActive() { batteryBoostActive() {
return ( return (

View file

@ -6,7 +6,7 @@
<div class="mb-3 row" data-testid="battery-boost"> <div class="mb-3 row" data-testid="battery-boost">
<label :for="formId('batteryBoost')" class="col-sm-4 col-form-label pt-0 pt-sm-2"> <label :for="formId('batteryBoost')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ $t("main.loadpointSettings.batteryBoost.label") }}&nbsp;🧪 {{ $t("main.loadpointSettings.batteryBoost.label") }}
</label> </label>
<div class="col-sm-8 pe-0"> <div class="col-sm-8 pe-0">
<div class="form-check form-switch my-1"> <div class="form-check form-switch my-1">

View file

@ -13,8 +13,6 @@
:error="callbackError" :error="callbackError"
:auth-providers="authProviders" :auth-providers="authProviders"
/> />
<WelcomeBanner v-if="loadpointsRequired" />
<ExperimentalBanner v-else-if="$hiddenFeatures()" />
<h2 class="my-4 mt-5">{{ $t("config.section.general") }}</h2> <h2 class="my-4 mt-5">{{ $t("config.section.general") }}</h2>
<GeneralConfig <GeneralConfig
@ -22,296 +20,291 @@
@site-changed="siteChanged" @site-changed="siteChanged"
/> />
<div v-if="$hiddenFeatures()"> <h2 class="my-4">{{ $t("config.section.loadpoints") }}</h2>
<h2 class="my-4">{{ $t("config.section.loadpoints") }} 🧪</h2> <WelcomeBanner v-if="loadpointsRequired" class="my-4" />
<p <div class="p-0 config-list">
v-if="loadpointsRequired" <DeviceCard
class="text-muted my-4" v-for="loadpoint in loadpoints"
data-testid="loadpoint-required" :key="loadpoint.name"
:title="loadpoint.title"
:name="loadpoint.name"
:editable="!!loadpoint.id"
:error="hasDeviceError('loadpoint', loadpoint.name)"
data-testid="loadpoint"
@edit="editLoadpoint(loadpoint.id)"
> >
{{ $t("config.main.loadpointRequired") }} <template #tags>
</p> <DeviceTags :tags="loadpointTags(loadpoint)" />
<div class="p-0 config-list"> </template>
<DeviceCard <template #icon>
v-for="loadpoint in loadpoints" <VehicleIcon
:key="loadpoint.name" v-if="chargerIcon(loadpoint.charger)"
:title="loadpoint.title" :name="chargerIcon(loadpoint.charger)"
:name="loadpoint.name" />
:editable="!!loadpoint.id" <LoadpointIcon v-else />
:error="hasDeviceError('loadpoint', loadpoint.name)" </template>
data-testid="loadpoint" </DeviceCard>
@edit="editLoadpoint(loadpoint.id)"
>
<template #tags>
<DeviceTags :tags="loadpointTags(loadpoint)" />
</template>
<template #icon>
<VehicleIcon
v-if="chargerIcon(loadpoint.charger)"
:name="chargerIcon(loadpoint.charger)"
/>
<LoadpointIcon v-else />
</template>
</DeviceCard>
<NewDeviceButton <NewDeviceButton
data-testid="add-loadpoint" data-testid="add-loadpoint"
:title="$t('config.main.addLoadpoint')" :title="$t('config.main.addLoadpoint')"
:attention="loadpointsRequired" :attention="loadpointsRequired"
@click="newLoadpoint" @click="newLoadpoint"
/> />
</div> </div>
<h2 class="my-4">{{ $t("config.section.vehicles") }} 🧪</h2> <h2 class="my-4">{{ $t("config.section.vehicles") }}</h2>
<div class="p-0 config-list"> <div class="p-0 config-list">
<DeviceCard <DeviceCard
v-for="vehicle in vehicles" v-for="vehicle in vehicles"
:key="vehicle.name" :key="vehicle.name"
:title="vehicle.config?.title || vehicle.name" :title="vehicle.config?.title || vehicle.name"
:name="vehicle.name" :name="vehicle.name"
:editable="vehicle.id >= 0" :editable="vehicle.id >= 0"
:error="hasDeviceError('vehicle', vehicle.name)" :error="hasDeviceError('vehicle', vehicle.name)"
data-testid="vehicle" data-testid="vehicle"
@edit="editVehicle(vehicle.id)" @edit="editVehicle(vehicle.id)"
> >
<template #icon> <template #icon>
<VehicleIcon :name="vehicle.config?.icon" /> <VehicleIcon :name="vehicle.config?.icon" />
</template> </template>
<template #tags> <template #tags>
<DeviceTags :tags="deviceTags('vehicle', vehicle.name)" /> <DeviceTags :tags="deviceTags('vehicle', vehicle.name)" />
</template> </template>
</DeviceCard> </DeviceCard>
<NewDeviceButton <NewDeviceButton
data-testid="add-vehicle" data-testid="add-vehicle"
:title="$t('config.main.addVehicle')" :title="$t('config.main.addVehicle')"
@click="newVehicle" @click="newVehicle"
/> />
</div> </div>
<h2 class="my-4 mt-5">{{ $t("config.section.grid") }} 🧪</h2> <h2 class="my-4 mt-5">{{ $t("config.section.grid") }}</h2>
<div class="p-0 config-list"> <div class="p-0 config-list">
<MeterCard <MeterCard
v-if="gridMeter" v-if="gridMeter"
:meter="gridMeter" :meter="gridMeter"
:title="$t('config.grid.title')" :title="$t('config.grid.title')"
meter-type="grid" meter-type="grid"
:has-error="hasDeviceError('meter', gridMeter.name)" :has-error="hasDeviceError('meter', gridMeter.name)"
:tags="deviceTags('meter', gridMeter.name)" :tags="deviceTags('meter', gridMeter.name)"
@edit="editMeter" @edit="editMeter"
/> />
<NewDeviceButton <NewDeviceButton
v-else v-else
:title="$t('config.main.addGrid')" :title="$t('config.main.addGrid')"
data-testid="add-grid" data-testid="add-grid"
@click="newMeter('grid')" @click="newMeter('grid')"
/> />
<DeviceCard <DeviceCard
v-if="tariffTags" :title="$t('config.tariffs.title')"
:title="$t('config.tariffs.title')" editable
editable :unconfigured="isUnconfigured(tariffTags)"
:error="hasClassError('tariff')" :error="hasClassError('tariff')"
data-testid="tariffs" data-testid="tariffs"
@edit="openModal('tariffsModal')" @edit="openModal('tariffsModal')"
> >
<template #icon> <template #icon>
<shopicon-regular-receivepayment></shopicon-regular-receivepayment> <shopicon-regular-receivepayment></shopicon-regular-receivepayment>
</template> </template>
<template #tags> <template #tags>
<DeviceTags :tags="tariffTags" /> <DeviceTags :tags="tariffTags" />
</template> </template>
</DeviceCard> </DeviceCard>
<NewDeviceButton </div>
v-else <h2 class="my-4 mt-5">{{ $t("config.section.meter") }}</h2>
:title="$t('config.main.addTariffs')" <div class="p-0 config-list">
data-testid="add-tariffs" <MeterCard
@click="openModal('tariffsModal')" v-for="meter in pvMeters"
/> :key="meter.name"
</div> :meter="meter"
<h2 class="my-4 mt-5">{{ $t("config.section.meter") }} 🧪</h2> meter-type="pv"
<div class="p-0 config-list"> :has-error="hasDeviceError('meter', meter.name)"
<MeterCard :tags="deviceTags('meter', meter.name)"
v-for="meter in pvMeters" @edit="editMeter"
:key="meter.name" />
:meter="meter" <MeterCard
meter-type="pv" v-for="meter in batteryMeters"
:has-error="hasDeviceError('meter', meter.name)" :key="meter.name"
:tags="deviceTags('meter', meter.name)" :meter="meter"
@edit="editMeter" meter-type="battery"
/> :has-error="hasDeviceError('meter', meter.name)"
<MeterCard :tags="deviceTags('meter', meter.name)"
v-for="meter in batteryMeters" @edit="editMeter"
:key="meter.name" />
:meter="meter" <NewDeviceButton
meter-type="battery" :title="$t('config.main.addPvBattery')"
:has-error="hasDeviceError('meter', meter.name)" @click="addSolarBatteryMeter"
:tags="deviceTags('meter', meter.name)" />
@edit="editMeter" </div>
/>
<NewDeviceButton
:title="$t('config.main.addPvBattery')"
@click="addSolarBatteryMeter"
/>
</div>
<h2 class="my-4 mt-5">{{ $t("config.section.additionalMeter") }} 🧪</h2> <h2 class="my-4 mt-5">{{ $t("config.section.additionalMeter") }}</h2>
<div class="p-0 config-list"> <div class="p-0 config-list">
<MeterCard <MeterCard
v-for="meter in auxMeters" v-for="meter in auxMeters"
:key="meter.name" :key="meter.name"
:meter="meter" :meter="meter"
meter-type="aux" meter-type="aux"
:has-error="hasDeviceError('meter', meter.name)" :has-error="hasDeviceError('meter', meter.name)"
:tags="deviceTags('meter', meter.name)" :tags="deviceTags('meter', meter.name)"
@edit="editMeter" @edit="editMeter"
/> />
<MeterCard <MeterCard
v-for="meter in extMeters" v-for="meter in extMeters"
:key="meter.name" :key="meter.name"
:meter="meter" :meter="meter"
meter-type="ext" meter-type="ext"
:has-error="hasDeviceError('meter', meter.name)" :has-error="hasDeviceError('meter', meter.name)"
:tags="deviceTags('meter', meter.name)" :tags="deviceTags('meter', meter.name)"
@edit="editMeter" @edit="editMeter"
/> />
<NewDeviceButton <NewDeviceButton
:title="$t('config.main.addAdditional')" :title="$t('config.main.addAdditional')"
@click="newAdditionalMeter" @click="newAdditionalMeter"
/> />
</div> </div>
<h2 class="my-4 mt-5">{{ $t("config.section.integrations") }} 🧪</h2> <h2 class="my-4 mt-5">{{ $t("config.section.integrations") }}</h2>
<div class="p-0 config-list"> <div class="p-0 config-list">
<AuthProvidersCard <AuthProvidersCard
:providers="authProviders" :providers="authProviders"
data-testid="auth-providers" data-testid="auth-providers"
@auth-request="handleProviderAuthRequest" @auth-request="handleProviderAuthRequest"
/> />
<DeviceCard <DeviceCard
:title="$t('config.mqtt.title')" :title="$t('config.mqtt.title')"
editable editable
:error="hasClassError('mqtt')" :error="hasClassError('mqtt')"
data-testid="mqtt" :unconfigured="isUnconfigured(mqttTags)"
@edit="openModal('mqttModal')" data-testid="mqtt"
> @edit="openModal('mqttModal')"
<template #icon><MqttIcon /></template> >
<template #tags> <template #icon><MqttIcon /></template>
<DeviceTags :tags="mqttTags" /> <template #tags>
</template> <DeviceTags :tags="mqttTags" />
</DeviceCard> </template>
<DeviceCard </DeviceCard>
:title="$t('config.messaging.title')" <DeviceCard
editable :title="$t('config.messaging.title')"
:error="hasClassError('messenger')" editable
data-testid="messaging" :error="hasClassError('messenger')"
@edit="openModal('messagingModal')" :unconfigured="isUnconfigured(messagingTags)"
> data-testid="messaging"
<template #icon><NotificationIcon /></template> @edit="openModal('messagingModal')"
<template #tags> >
<DeviceTags :tags="messagingTags" /> <template #icon><NotificationIcon /></template>
</template> <template #tags>
</DeviceCard> <DeviceTags :tags="messagingTags" />
<DeviceCard </template>
:title="$t('config.influx.title')" </DeviceCard>
editable <DeviceCard
:error="hasClassError('influx')" :title="$t('config.influx.title')"
data-testid="influx" editable
@edit="openModal('influxModal')" :error="hasClassError('influx')"
> :unconfigured="isUnconfigured(influxTags)"
<template #icon><InfluxIcon /></template> data-testid="influx"
<template #tags> @edit="openModal('influxModal')"
<DeviceTags :tags="influxTags" /> >
</template> <template #icon><InfluxIcon /></template>
</DeviceCard> <template #tags>
<DeviceCard <DeviceTags :tags="influxTags" />
:title="`${$t('config.eebus.title')} 🧪`" </template>
editable </DeviceCard>
:error="hasClassError('eebus')" <DeviceCard
data-testid="eebus" :title="$t('config.eebus.title')"
@edit="openModal('eebusModal')" editable
> :error="hasClassError('eebus')"
<template #icon><EebusIcon /></template> :unconfigured="isUnconfigured(eebusTags)"
<template #tags> data-testid="eebus"
<DeviceTags :tags="eebusTags" /> @edit="openModal('eebusModal')"
</template> >
</DeviceCard> <template #icon><EebusIcon /></template>
<DeviceCard <template #tags>
:title="$t('config.ocpp.title')" <DeviceTags :tags="eebusTags" />
editable </template>
:error="hasClassError('ocpp')" </DeviceCard>
data-testid="ocpp" <DeviceCard
@edit="openModal('ocppModal')" :title="$t('config.ocpp.title')"
> editable
<template #icon><OcppIcon /></template> :error="hasClassError('ocpp')"
<template #tags> :unconfigured="isUnconfigured(ocppTags)"
<DeviceTags :tags="ocppTags" /> data-testid="ocpp"
</template> @edit="openModal('ocppModal')"
</DeviceCard> >
<template #icon><OcppIcon /></template>
<template #tags>
<DeviceTags :tags="ocppTags" />
</template>
</DeviceCard>
<DeviceCard <DeviceCard
:title="`${$t('config.circuits.title')} 🧪`" :title="`${$t('config.circuits.title')}`"
editable editable
:error="hasClassError('circuit')" :error="hasClassError('circuit')"
data-testid="circuits" :unconfigured="circuitsSorted.length === 0"
@edit="openModal('circuitsModal')" data-testid="circuits"
> @edit="openModal('circuitsModal')"
<template #icon><CircuitsIcon /></template> >
<template #tags> <template #icon><CircuitsIcon /></template>
<DeviceTags <template #tags>
v-if="circuitsSorted.length == 0" <DeviceTags
:tags="{ configured: { value: false } }" v-if="circuitsSorted.length == 0"
/> :tags="{ configured: { value: false } }"
<template />
v-for="(circuit, idx) in circuitsSorted" <template
v-else v-for="(circuit, idx) in circuitsSorted"
:key="circuit.name" v-else
> :key="circuit.name"
<hr v-if="idx > 0" /> >
<p class="my-2 fw-bold"> <hr v-if="idx > 0" />
{{ circuit.config?.title }} <p class="my-2 fw-bold">
<code>({{ circuit.name }})</code> {{ circuit.config?.title }}
</p> <code>({{ circuit.name }})</code>
<DeviceTags :tags="circuitTags(circuit)" /> </p>
</template> <DeviceTags :tags="circuitTags(circuit)" />
</template> </template>
</DeviceCard> </template>
<DeviceCard </DeviceCard>
:title="$t('config.modbusproxy.title')" <DeviceCard
editable :title="$t('config.modbusproxy.title')"
:error="hasClassError('modbusproxy')" editable
data-testid="modbusproxy" :error="hasClassError('modbusproxy')"
@edit="openModal('modbusProxyModal')" :unconfigured="isUnconfigured(modbusproxyTags)"
> data-testid="modbusproxy"
<template #icon><ModbusProxyIcon /></template> @edit="openModal('modbusProxyModal')"
<template #tags> >
<DeviceTags :tags="modbusproxyTags" /> <template #icon><ModbusProxyIcon /></template>
</template> <template #tags>
</DeviceCard> <DeviceTags :tags="modbusproxyTags" />
<DeviceCard </template>
:title="$t('config.shm.cardTitle')" </DeviceCard>
editable <DeviceCard
:error="hasClassError('shm')" :title="$t('config.shm.cardTitle')"
data-testid="shm" editable
@edit="openModal('shmModal')" :error="hasClassError('shm')"
> :unconfigured="isUnconfigured(shmTags)"
<template #icon><ShmIcon /></template> data-testid="shm"
<template #tags> @edit="openModal('shmModal')"
<DeviceTags :tags="shmTags" /> >
</template> <template #icon><ShmIcon /></template>
</DeviceCard> <template #tags>
<DeviceCard <DeviceTags :tags="shmTags" />
:title="$t('config.hems.title')" </template>
editable </DeviceCard>
:error="hasClassError('hems')" <DeviceCard
data-testid="hems" :title="$t('config.hems.title')"
@edit="openModal('hemsModal')" editable
> :error="hasClassError('hems')"
<template #icon><HemsIcon /></template> :unconfigured="isUnconfigured(hemsTags)"
<template #tags> data-testid="hems"
<DeviceTags :tags="hemsTags" /> @edit="openModal('hemsModal')"
</template> >
</DeviceCard> <template #icon><HemsIcon /></template>
</div> <template #tags>
<DeviceTags :tags="hemsTags" />
</template>
</DeviceCard>
</div> </div>
<hr class="my-5" /> <hr class="my-5" />
@ -387,6 +380,7 @@
<MessagingModal @changed="yamlChanged" /> <MessagingModal @changed="yamlChanged" />
<TariffsModal @changed="yamlChanged" /> <TariffsModal @changed="yamlChanged" />
<TelemetryModal :sponsor="sponsor" :telemetry="telemetry" /> <TelemetryModal :sponsor="sponsor" :telemetry="telemetry" />
<ExperimentalModal />
<ModbusProxyModal :is-sponsor="isSponsor" @changed="loadDirty" /> <ModbusProxyModal :is-sponsor="isSponsor" @changed="loadDirty" />
<CircuitsModal <CircuitsModal
:gridMeter="gridMeter" :gridMeter="gridMeter"
@ -446,6 +440,7 @@ import SponsorModal from "../components/Config/SponsorModal.vue";
import store from "../store"; import store from "../store";
import TariffsModal from "../components/Config/TariffsModal.vue"; import TariffsModal from "../components/Config/TariffsModal.vue";
import TelemetryModal from "../components/Config/TelemetryModal.vue"; import TelemetryModal from "../components/Config/TelemetryModal.vue";
import ExperimentalModal from "../components/Config/ExperimentalModal.vue";
import Header from "../components/Top/Header.vue"; import Header from "../components/Top/Header.vue";
import VehicleIcon from "../components/VehicleIcon"; import VehicleIcon from "../components/VehicleIcon";
import VehicleModal from "../components/Config/VehicleModal.vue"; import VehicleModal from "../components/Config/VehicleModal.vue";
@ -466,9 +461,14 @@ import type {
} from "@/types/evcc"; } from "@/types/evcc";
type DeviceValuesMap = Record<DeviceType, Record<string, any>>; type DeviceValuesMap = Record<DeviceType, Record<string, any>>;
type DeviceTags = Record<
string,
{ value?: any; error?: boolean; warning?: boolean; muted?: boolean; options?: any }
>;
import BackupRestoreModal from "@/components/Config/BackupRestoreModal.vue"; import BackupRestoreModal from "@/components/Config/BackupRestoreModal.vue";
import WelcomeBanner from "../components/Config/WelcomeBanner.vue"; import WelcomeBanner from "../components/Config/WelcomeBanner.vue";
import ExperimentalBanner from "../components/Config/ExperimentalBanner.vue";
import AuthSuccessBanner from "../components/Config/AuthSuccessBanner.vue"; import AuthSuccessBanner from "../components/Config/AuthSuccessBanner.vue";
import PasswordModal from "../components/Auth/PasswordModal.vue"; import PasswordModal from "../components/Auth/PasswordModal.vue";
import AuthProvidersCard from "../components/Config/AuthProvidersCard.vue"; import AuthProvidersCard from "../components/Config/AuthProvidersCard.vue";
@ -488,8 +488,6 @@ export default defineComponent({
EebusModal, EebusModal,
OcppIcon, OcppIcon,
OcppModal, OcppModal,
ExperimentalBanner,
AuthSuccessBanner,
GeneralConfig, GeneralConfig,
HemsIcon, HemsIcon,
HemsModal, HemsModal,
@ -511,10 +509,12 @@ export default defineComponent({
SponsorModal, SponsorModal,
TariffsModal, TariffsModal,
TelemetryModal, TelemetryModal,
ExperimentalModal,
TopHeader: Header, TopHeader: Header,
VehicleIcon, VehicleIcon,
VehicleModal, VehicleModal,
WelcomeBanner, WelcomeBanner,
AuthSuccessBanner,
PasswordModal, PasswordModal,
AuthProvidersCard, AuthProvidersCard,
}, },
@ -602,7 +602,7 @@ export default defineComponent({
selectedChargerName() { selectedChargerName() {
return this.getChargerById(this.selectedChargerId)?.name; return this.getChargerById(this.selectedChargerId)?.name;
}, },
tariffTags() { tariffTags(): DeviceTags {
const { currency, tariffGrid, tariffFeedIn, tariffCo2, tariffSolar } = store.state; const { currency, tariffGrid, tariffFeedIn, tariffCo2, tariffSolar } = store.state;
if ( if (
tariffGrid === undefined && tariffGrid === undefined &&
@ -610,7 +610,7 @@ export default defineComponent({
tariffCo2 === undefined && tariffCo2 === undefined &&
tariffSolar === undefined tariffSolar === undefined
) { ) {
return null; return { configured: { value: false } };
} }
const tags = { const tags = {
currency: {}, currency: {},
@ -636,7 +636,7 @@ export default defineComponent({
} }
return tags; return tags;
}, },
mqttTags() { mqttTags(): DeviceTags {
const { broker, topic } = store.state?.mqtt || {}; const { broker, topic } = store.state?.mqtt || {};
if (!broker) return { configured: { value: false } }; if (!broker) return { configured: { value: false } };
return { return {
@ -644,7 +644,7 @@ export default defineComponent({
topic: { value: topic }, topic: { value: topic },
}; };
}, },
influxTags() { influxTags(): DeviceTags {
const { url, database, org } = store.state?.influx || {}; const { url, database, org } = store.state?.influx || {};
if (!url) return { configured: { value: false } }; if (!url) return { configured: { value: false } };
const result = { url: { value: url }, bucket: {}, org: {} }; const result = { url: { value: url }, bucket: {}, org: {} };
@ -655,13 +655,16 @@ export default defineComponent({
vehicleOptions() { vehicleOptions() {
return this.vehicles.map((v) => ({ key: v.name, name: v.config?.title || v.name })); return this.vehicles.map((v) => ({ key: v.name, name: v.config?.title || v.name }));
}, },
shmTags() { shmTags(): DeviceTags {
return { configured: { value: true } }; const { vendorId, deviceId } = store.state?.shm || {};
// TODO: use incoming SEMP connections to determin configured/active status
const value = !!vendorId || !!deviceId;
return { configured: { value } };
}, },
hems() { hems() {
return store.state?.hems; return store.state?.hems;
}, },
hemsTags() { hemsTags(): DeviceTags {
const type = this.hems?.config?.type; const type = this.hems?.config?.type;
if (!type) { if (!type) {
return { configured: { value: false } }; return { configured: { value: false } };
@ -694,10 +697,10 @@ export default defineComponent({
// @ts-expect-error: telemetry property exists but not in TypeScript definitions // @ts-expect-error: telemetry property exists but not in TypeScript definitions
return store.state?.telemetry === true; return store.state?.telemetry === true;
}, },
eebusTags() { eebusTags(): DeviceTags {
return { configured: { value: store.state?.eebus || false } }; return { configured: { value: store.state?.eebus || false } };
}, },
ocppTags() { ocppTags(): DeviceTags {
const ocpp = store.state?.ocpp; const ocpp = store.state?.ocpp;
const stations = ocpp?.status?.stations || []; const stations = ocpp?.status?.stations || [];
if (stations.length === 0) { if (stations.length === 0) {
@ -719,14 +722,14 @@ export default defineComponent({
return tags; return tags;
}, },
modbusproxyTags() { modbusproxyTags(): DeviceTags {
const config = store.state?.modbusproxy || []; const config = store.state?.modbusproxy || [];
if (config.length > 0) { if (config.length > 0) {
return { amount: { value: config.length } }; return { amount: { value: config.length } };
} }
return { configured: { value: false } }; return { configured: { value: false } };
}, },
messagingTags() { messagingTags(): DeviceTags {
return { configured: { value: store.state?.messaging || false } }; return { configured: { value: store.state?.messaging || false } };
}, },
backupRestoreProps() { backupRestoreProps() {
@ -762,6 +765,9 @@ export default defineComponent({
} }
}, },
methods: { methods: {
isUnconfigured(tags: DeviceTags): boolean {
return tags["configured"]?.value === false;
},
handleVisibilityChange() { handleVisibilityChange() {
this.isPageVisible = document.visibilityState === "visible"; this.isPageVisible = document.visibilityState === "visible";
if (this.isPageVisible) { if (this.isPageVisible) {

View file

@ -175,6 +175,10 @@
"description": "Grundkonfiguration für die Kommunikation mit anderen EEBus-Geräten.", "description": "Grundkonfiguration für die Kommunikation mit anderen EEBus-Geräten.",
"title": "EEBus" "title": "EEBus"
}, },
"experimental": {
"description": "Aktiviert die Benutzeroberfläche für Funktionen, die sich noch in der Testphase befinden und sich in zukünftigen Versionen ändern können.",
"title": "Experimentell"
},
"ext": { "ext": {
"description": "Erfasst Energiewerte ungeregelter Verbraucher (bspw. Kühlschrank, Waschmaschine, etc.) für Statistikzwecke. Diese Zähler können auch für Lastmanagement verwendet werden (bspw. Unterverteilung).", "description": "Erfasst Energiewerte ungeregelter Verbraucher (bspw. Kühlschrank, Waschmaschine, etc.) für Statistikzwecke. Diese Zähler können auch für Lastmanagement verwendet werden (bspw. Unterverteilung).",
"titleAdd": "Verbrauchszähler hinzufügen", "titleAdd": "Verbrauchszähler hinzufügen",
@ -383,6 +387,8 @@
"title": "Konfiguration", "title": "Konfiguration",
"unconfigured": "nicht konfiguriert", "unconfigured": "nicht konfiguriert",
"vehicles": "Meine Fahrzeuge", "vehicles": "Meine Fahrzeuge",
"welcomeBannerText": "Lege zunächst eine **Wallbox oder Heizung** an. Falls du noch keine echte Wallbox hast, wähle eine **Demowallbox**.",
"welcomeBannerTitle": "Los geht's mit der Konfiguration!",
"yaml": "Geräte aus evcc.yaml sind nicht editierbar." "yaml": "Geräte aus evcc.yaml sind nicht editierbar."
}, },
"messaging": { "messaging": {
@ -1201,7 +1207,7 @@
}, },
"hiddenFeatures": { "hiddenFeatures": {
"label": "Experimentell", "label": "Experimentell",
"value": "Experimentelle UI-Funktionen zeigen." "value": "Experimentelle Funktionen anzeigen."
}, },
"language": { "language": {
"auto": "Automatisch", "auto": "Automatisch",

View file

@ -175,6 +175,10 @@
"description": "Configuration that enables evcc to communicate with other EEBus devices.", "description": "Configuration that enables evcc to communicate with other EEBus devices.",
"title": "EEBus" "title": "EEBus"
}, },
"experimental": {
"description": "Enable user interface for features that are still being tested and may change in future versions.",
"title": "Experimental"
},
"ext": { "ext": {
"description": "Records energy values of uncontrolled consumers (e.g. refrigerator, washing machine, etc.) for statistical purposes. These meters can also be used for load management (e.g. sub-distribution).", "description": "Records energy values of uncontrolled consumers (e.g. refrigerator, washing machine, etc.) for statistical purposes. These meters can also be used for load management (e.g. sub-distribution).",
"titleAdd": "Add Consumer Meter", "titleAdd": "Add Consumer Meter",
@ -383,6 +387,8 @@
"title": "Configuration", "title": "Configuration",
"unconfigured": "not configured", "unconfigured": "not configured",
"vehicles": "My Vehicles", "vehicles": "My Vehicles",
"welcomeBannerText": "Start with creating a **charger or heater**. If you don't have a real charger yet, choose a **demo charger**.",
"welcomeBannerTitle": "Let's configure your system!",
"yaml": "Devices from evcc.yaml are not editable." "yaml": "Devices from evcc.yaml are not editable."
}, },
"messaging": { "messaging": {
@ -1201,7 +1207,7 @@
}, },
"hiddenFeatures": { "hiddenFeatures": {
"label": "Experimental", "label": "Experimental",
"value": "Show experimental UI features." "value": "Show experimental features."
}, },
"language": { "language": {
"auto": "Automatic", "auto": "Automatic",

View file

@ -1,11 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, baseUrl, restart } from "./evcc"; import { start, stop, baseUrl, restart } from "./evcc";
import { import { openTopNavigation, expectModalVisible, expectModalHidden } from "./utils";
enableExperimental,
openTopNavigation,
expectModalVisible,
expectModalHidden,
} from "./utils";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@ -22,7 +17,6 @@ test.describe("reset", async () => {
// open backup & restore modal // open backup & restore modal
await openTopNavigation(page); await openTopNavigation(page);
await page.getByRole("link", { name: "Configuration" }).click(); await page.getByRole("link", { name: "Configuration" }).click();
await enableExperimental(page);
await page.getByRole("button", { name: "Backup & Restore" }).click(); await page.getByRole("button", { name: "Backup & Restore" }).click();
const modal = page.getByTestId("backup-restore-modal"); const modal = page.getByTestId("backup-restore-modal");
await expectModalVisible(modal); await expectModalVisible(modal);
@ -56,7 +50,6 @@ test.describe("reset", async () => {
// create grid meter and title via UI // create grid meter and title via UI
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
await page.getByTestId("add-grid").click(); await page.getByTestId("add-grid").click();
const meterModal = page.getByTestId("meter-modal"); const meterModal = page.getByTestId("meter-modal");
await expectModalVisible(meterModal); await expectModalVisible(meterModal);
@ -119,7 +112,6 @@ test.describe("backup and restore", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// set initial title // set initial title
await page.getByTestId("generalconfig-title").getByRole("button", { name: "edit" }).click(); await page.getByTestId("generalconfig-title").getByRole("button", { name: "edit" }).click();
@ -224,8 +216,6 @@ test.describe("backup and restore", async () => {
await loginModal.getByRole("button", { name: "Login" }).click(); await loginModal.getByRole("button", { name: "Login" }).click();
await expectModalHidden(loginModal); await expectModalHidden(loginModal);
await enableExperimental(page);
// open backup & restore modal // open backup & restore modal
await page.getByRole("button", { name: "Backup & Restore" }).click(); await page.getByRole("button", { name: "Backup & Restore" }).click();
const backupModal = page.getByTestId("backup-restore-modal"); const backupModal = page.getByTestId("backup-restore-modal");

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc"; import { start, stop, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden } from "./utils"; import { expectModalHidden } from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
test.beforeAll(async () => { test.beforeAll(async () => {
@ -23,7 +23,6 @@ test.describe("boost", async () => {
}); });
test("activate boost in solar mode", async ({ page }) => { test("activate boost in solar mode", async ({ page }) => {
await enableExperimental(page, false);
await expect(page.getByTestId("vehicle-status-batteryboost")).not.toBeVisible(); await expect(page.getByTestId("vehicle-status-batteryboost")).not.toBeVisible();
await page await page
.getByTestId("mode") .getByTestId("mode")
@ -42,7 +41,6 @@ test.describe("boost", async () => {
}); });
test("disabled in fast mode", async ({ page }) => { test("disabled in fast mode", async ({ page }) => {
await enableExperimental(page, false);
await page.getByTestId("mode").first().getByRole("button", { name: "Fast" }).click(); await page.getByTestId("mode").first().getByRole("button", { name: "Fast" }).click();
await page.getByTestId("loadpoint-settings-button").nth(1).click(); await page.getByTestId("loadpoint-settings-button").nth(1).click();
const modal = page.getByTestId("loadpoint-settings-modal"); const modal = page.getByTestId("loadpoint-settings-modal");

View file

@ -1,12 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { import { editorClear, editorPaste, expectModalHidden, expectModalVisible } from "./utils";
editorClear,
editorPaste,
enableExperimental,
expectModalHidden,
expectModalVisible,
} from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml"; const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
@ -22,7 +16,6 @@ test.afterEach(async () => {
test.describe("aux meter", async () => { test.describe("aux meter", async () => {
test("create and remove aux meter", async ({ page }) => { test("create and remove aux meter", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("aux")).toHaveCount(0); await expect(page.getByTestId("aux")).toHaveCount(0);
@ -67,7 +60,6 @@ test.describe("aux meter", async () => {
test("user-defined meter", async ({ page }) => { test("user-defined meter", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await page.getByRole("button", { name: "Add additional meter" }).click(); await page.getByRole("button", { name: "Add additional meter" }).click();
const modal = page.getByTestId("meter-modal"); const modal = page.getByTestId("meter-modal");
@ -160,7 +152,6 @@ energy:
test("user-defined meter with errors", async ({ page }) => { test("user-defined meter with errors", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await page.getByRole("button", { name: "Add additional meter" }).click(); await page.getByRole("button", { name: "Add additional meter" }).click();

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils"; import { expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml"; const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
@ -16,7 +16,6 @@ test.afterAll(async () => {
test.describe("battery meter", async () => { test.describe("battery meter", async () => {
test("create, edit and remove battery meter", async ({ page }) => { test("create, edit and remove battery meter", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("battery")).toHaveCount(0); await expect(page.getByTestId("battery")).toHaveCount(0);
@ -69,7 +68,6 @@ test.describe("battery meter", async () => {
test("advanced fields", async ({ page }) => { test("advanced fields", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await page.getByRole("button", { name: "Add solar or battery" }).click(); await page.getByRole("button", { name: "Add solar or battery" }).click();

View file

@ -1,12 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { import { expectModalVisible, expectModalHidden, editorClear, editorPaste } from "./utils";
enableExperimental,
expectModalVisible,
expectModalHidden,
editorClear,
editorPaste,
} from "./utils";
const CONFIG_YAML = "config-circuit.evcc.yaml"; const CONFIG_YAML = "config-circuit.evcc.yaml";
@ -21,7 +15,6 @@ test.describe("circuit", async () => {
await start(CONFIG_YAML); await start(CONFIG_YAML);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("loadpoint")).toHaveCount(1); await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText(["Power", "1.0 kW"].join("")); await expect(page.getByTestId("loadpoint")).toContainText(["Power", "1.0 kW"].join(""));
@ -43,7 +36,6 @@ test.describe("circuit", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add grid meter // add grid meter
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { enableExperimental, expectModalVisible, expectModalHidden } from "./utils"; import { expectModalVisible, expectModalHidden } from "./utils";
import { simulatorUrl, startSimulator, stopSimulator } from "./simulator"; import { simulatorUrl, startSimulator, stopSimulator } from "./simulator";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -30,7 +30,6 @@ test.afterEach(async () => {
test.describe("config device auth", async () => { test.describe("config device auth", async () => {
test("create grid meter with redirect auth", async ({ page }) => { test("create grid meter with redirect auth", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
// verify no grid meter exists yet // verify no grid meter exists yet
await expect(page.getByTestId("grid")).toHaveCount(0); await expect(page.getByTestId("grid")).toHaveCount(0);
@ -133,7 +132,6 @@ test.describe("config device auth", async () => {
test("create grid meter with device-code auth", async ({ page }) => { test("create grid meter with device-code auth", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
// create a grid meter with device-code auth // create a grid meter with device-code auth
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();
@ -156,7 +154,6 @@ test.describe("config device auth", async () => {
test("error server shows auth error", async ({ page }) => { test("error server shows auth error", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();
const meterModal = page.getByTestId("meter-modal"); const meterModal = page.getByTestId("meter-modal");
@ -194,7 +191,6 @@ test.describe("config device auth", async () => {
test("user denies authorization shows error banner", async ({ page }) => { test("user denies authorization shows error banner", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
// create a grid meter with auth // create a grid meter with auth
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();
@ -231,7 +227,6 @@ test.describe("config device auth", async () => {
test("authorization card connect and disconnect flow", async ({ page }) => { test("authorization card connect and disconnect flow", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
// create a grid meter with auth and prepare connection // create a grid meter with auth and prepare connection
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();

View file

@ -1,12 +1,6 @@
import { test, expect, type Page } from "@playwright/test"; import { test, expect, type Page } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { import { editorClear, editorPaste, expectModalHidden, expectModalVisible } from "./utils";
editorClear,
editorPaste,
enableExperimental,
expectModalHidden,
expectModalVisible,
} from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml"; const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
const CONFIG_BASICS = "basics.evcc.yaml"; const CONFIG_BASICS = "basics.evcc.yaml";
@ -37,7 +31,6 @@ test.describe("ext meter", async () => {
test("template-based ext meter", async ({ page }) => { test("template-based ext meter", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("ext")).toHaveCount(0); await expect(page.getByTestId("ext")).toHaveCount(0);
await page.getByRole("button", { name: "Add additional meter" }).click(); await page.getByRole("button", { name: "Add additional meter" }).click();
@ -104,7 +97,6 @@ test.describe("ext meter", async () => {
test("switch from template to custom ext meter", async ({ page }) => { test("switch from template to custom ext meter", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await page.getByRole("button", { name: "Add additional meter" }).click(); await page.getByRole("button", { name: "Add additional meter" }).click();
const meterModal = page.getByTestId("meter-modal"); const meterModal = page.getByTestId("meter-modal");
@ -187,7 +179,6 @@ test.describe("ext meter order", async () => {
test("ensure order is preserved", async ({ page }) => { test("ensure order is preserved", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("ext")).toHaveCount(0); await expect(page.getByTestId("ext")).toHaveCount(0);
// Create meters // Create meters

View file

@ -1,13 +1,7 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorHost } from "./simulator"; import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
import { import { expectModalVisible, expectModalHidden, addDemoCharger, newLoadpoint } from "./utils";
expectModalVisible,
expectModalHidden,
enableExperimental,
addDemoCharger,
newLoadpoint,
} from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -21,7 +15,6 @@ test.describe("fatal config handling", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
// create meter // create meter
await page.getByRole("button", { name: "Add solar or battery" }).click(); await page.getByRole("button", { name: "Add solar or battery" }).click();
@ -61,7 +54,6 @@ test.describe("fatal config handling", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
const lpModal = page.getByTestId("loadpoint-modal"); const lpModal = page.getByTestId("loadpoint-modal");
@ -124,7 +116,6 @@ test.describe("fatal config handling", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
// create grid meter // create grid meter
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();

View file

@ -7,7 +7,7 @@ import {
simulatorHost, simulatorHost,
simulatorApply, simulatorApply,
} from "./simulator"; } from "./simulator";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils"; import { expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_ONE_LP = "config-one-lp.evcc.yaml"; const CONFIG_ONE_LP = "config-one-lp.evcc.yaml";
@ -38,7 +38,6 @@ test.describe("grid meter", async () => {
await simulatorApply(page); await simulatorApply(page);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("grid")).toHaveCount(0); await expect(page.getByTestId("grid")).toHaveCount(0);
await expect(page.getByTestId("add-grid")).toBeVisible(); await expect(page.getByTestId("add-grid")).toBeVisible();

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden, enableExperimental } from "./utils"; import { expectModalVisible, expectModalHidden } from "./utils";
const CONFIG_INVALID_TEMPLATE = "config-invalid-template.sql"; const CONFIG_INVALID_TEMPLATE = "config-invalid-template.sql";
@ -15,7 +15,6 @@ test.describe("invalid template migration", async () => {
await start(undefined, CONFIG_INVALID_TEMPLATE); await start(undefined, CONFIG_INVALID_TEMPLATE);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
// startup error // startup error
await expect(page.getByTestId("fatal-error")).toBeVisible(); await expect(page.getByTestId("fatal-error")).toBeVisible();

View file

@ -5,7 +5,6 @@ import {
expectModalHidden, expectModalHidden,
editorClear, editorClear,
editorPaste, editorPaste,
enableExperimental,
LoadpointType, LoadpointType,
addDemoCharger, addDemoCharger,
addDemoMeter, addDemoMeter,
@ -27,7 +26,6 @@ test.describe("charging loadpoint", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
const lpModal = page.getByTestId("loadpoint-modal"); const lpModal = page.getByTestId("loadpoint-modal");
const chargerModal = page.getByTestId("charger-modal"); const chargerModal = page.getByTestId("charger-modal");
@ -137,7 +135,6 @@ test.describe("charging loadpoint", async () => {
test("priority", async ({ page }) => { test("priority", async ({ page }) => {
await start(CONFIG_ONE_LP); await start(CONFIG_ONE_LP);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
const lpModal = page.getByTestId("loadpoint-modal"); const lpModal = page.getByTestId("loadpoint-modal");
@ -191,7 +188,6 @@ test.describe("charging loadpoint", async () => {
test("vehicle", async ({ page }) => { test("vehicle", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
const LP_1 = "Carport"; const LP_1 = "Carport";
const LP_2 = "Garage"; const LP_2 = "Garage";
@ -252,7 +248,6 @@ test.describe("charging loadpoint", async () => {
test("keep mode", async ({ page }) => { test("keep mode", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add grid meter // add grid meter
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();
@ -300,7 +295,6 @@ test.describe("charging loadpoint", async () => {
test("delete vehicle references", async ({ page }) => { test("delete vehicle references", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add vehicle, add loadpoint, add charger // add vehicle, add loadpoint, add charger
await addVehicle(page, "Porsche"); await addVehicle(page, "Porsche");
@ -333,7 +327,6 @@ test.describe("charging loadpoint", async () => {
test("delete charger references", async ({ page }) => { test("delete charger references", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint, add charger // add loadpoint, add charger
await newLoadpoint(page, "Garage"); await newLoadpoint(page, "Garage");
@ -368,7 +361,6 @@ test.describe("charging loadpoint", async () => {
test("delete meter references", async ({ page }) => { test("delete meter references", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint, add charger // add loadpoint, add charger
await newLoadpoint(page, "Garage"); await newLoadpoint(page, "Garage");
@ -403,7 +395,6 @@ test.describe("charging loadpoint", async () => {
test("user-defined charger", async ({ page }) => { test("user-defined charger", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint // add loadpoint
await newLoadpoint(page, "Carport"); await newLoadpoint(page, "Carport");
@ -517,7 +508,6 @@ test.describe("heating loadpoint", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint // add loadpoint
await newLoadpoint(page, "Wärmepumpe", LoadpointType.Heating); await newLoadpoint(page, "Wärmepumpe", LoadpointType.Heating);
@ -560,7 +550,6 @@ test.describe("heating loadpoint", async () => {
test("user-defined heat pump", async ({ page }) => { test("user-defined heat pump", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint // add loadpoint
await newLoadpoint(page, "Wärmepumpe", LoadpointType.Heating); await newLoadpoint(page, "Wärmepumpe", LoadpointType.Heating);
@ -634,7 +623,6 @@ temp:
test("delete charger/heater reverts to loadpoint type select screen", async ({ page }) => { test("delete charger/heater reverts to loadpoint type select screen", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint // add loadpoint
await newLoadpoint(page, "Wärmepumpe", LoadpointType.Heating); await newLoadpoint(page, "Wärmepumpe", LoadpointType.Heating);
@ -662,7 +650,6 @@ test.describe("sponsor token", () => {
test("create charger with missing sponsor token", async ({ page }) => { test("create charger with missing sponsor token", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// add loadpoint with Peblar charger // add loadpoint with Peblar charger
await newLoadpoint(page, "Peblar Test Charger"); await newLoadpoint(page, "Peblar Test Charger");

View file

@ -1,6 +1,6 @@
import { test, expect, type Page } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc"; import { start, stop, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils"; import { expectModalHidden, expectModalVisible } from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -10,15 +10,10 @@ test.afterEach(async () => {
const SELECT_ALL = "ControlOrMeta+KeyA"; const SELECT_ALL = "ControlOrMeta+KeyA";
async function goToConfig(page: Page) {
await page.goto("/#/config");
await enableExperimental(page);
}
test.describe("messaging", async () => { test.describe("messaging", async () => {
test("save a comment", async ({ page }) => { test("save a comment", async ({ page }) => {
await start(); await start();
await goToConfig(page); await page.goto("/#/config");
await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click(); await page.getByTestId("messaging").getByRole("button", { name: "edit" }).click();
const modal = await page.getByTestId("messaging-modal"); const modal = await page.getByTestId("messaging-modal");

View file

@ -1,6 +1,6 @@
import { test, expect, type Page, type Locator } from "@playwright/test"; import { test, expect, type Page, type Locator } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc"; import { start, stop, baseUrl } from "./evcc";
import { expectModalVisible, enableExperimental } from "./utils"; import { expectModalVisible } from "./utils";
const CONFIG_MODBUS_FIELDS = "config-modbus-fields.sql"; const CONFIG_MODBUS_FIELDS = "config-modbus-fields.sql";
@ -16,7 +16,6 @@ test.afterAll(async () => {
async function openMeterModal(page: Page, title: string): Promise<Locator> { async function openMeterModal(page: Page, title: string): Promise<Locator> {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
await page await page
.getByTestId("pv") .getByTestId("pv")
.filter({ hasText: title }) .filter({ hasText: title })

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, baseUrl, restart } from "./evcc"; import { start, stop, baseUrl, restart } from "./evcc";
import { expectModalVisible, enableExperimental, expectModalHidden } from "./utils"; import { expectModalVisible, expectModalHidden } from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
test.describe.configure({ mode: "parallel" }); test.describe.configure({ mode: "parallel" });
@ -15,7 +15,6 @@ test.describe("modbusproxy", async () => {
test("modbusproxy not configured", async ({ page }) => { test("modbusproxy not configured", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
const modbusproxyCard = page.getByTestId("modbusproxy"); const modbusproxyCard = page.getByTestId("modbusproxy");
@ -26,7 +25,6 @@ test.describe("modbusproxy", async () => {
test("modbusproxy via ui", async ({ page }) => { test("modbusproxy via ui", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
// add connection // add connection
const modbusproxyCard = page.getByTestId("modbusproxy"); const modbusproxyCard = page.getByTestId("modbusproxy");
@ -92,7 +90,6 @@ test.describe("modbusproxy", async () => {
test("modbusproxy via db (yaml to json migration)", async ({ page }) => { test("modbusproxy via db (yaml to json migration)", async ({ page }) => {
await start(undefined, CONFIG_MODBUSPROXY_MIGRATE); await start(undefined, CONFIG_MODBUSPROXY_MIGRATE);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
const modbusproxyCard = page.getByTestId("modbusproxy"); const modbusproxyCard = page.getByTestId("modbusproxy");
await expect(modbusproxyCard).toContainText(["Amount", "3"].join("")); await expect(modbusproxyCard).toContainText(["Amount", "3"].join(""));

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils"; import { expectModalHidden, expectModalVisible } from "./utils";
import { isMqttReachable } from "./mqtt"; import { isMqttReachable } from "./mqtt";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -9,7 +9,6 @@ test.describe.configure({ mode: "parallel" });
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
}); });
test.afterEach(async () => { test.afterEach(async () => {

View file

@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc"; import { start, stop, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorUrl } from "./simulator"; import { startSimulator, stopSimulator, simulatorUrl } from "./simulator";
import { expectModalVisible, enableExperimental } from "./utils"; import { expectModalVisible } from "./utils";
import axios from "axios"; import axios from "axios";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -22,7 +22,6 @@ test.describe("ocpp", () => {
test("ocpp modal", async ({ page }) => { test("ocpp modal", async ({ page }) => {
// Navigate to config page // Navigate to config page
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
// Open OCPP card // Open OCPP card
const ocppCard = page.getByTestId("ocpp"); const ocppCard = page.getByTestId("ocpp");
@ -55,7 +54,6 @@ test.describe("ocpp", () => {
test("create ocpp charger", async ({ page }) => { test("create ocpp charger", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
// Open loadpoint modal and select charging point type // Open loadpoint modal and select charging point type
await page.getByRole("button", { name: "Add charger or heater" }).click(); await page.getByRole("button", { name: "Add charger or heater" }).click();

View file

@ -39,13 +39,9 @@ test.describe("onboarding", async () => {
// config page // config page
await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible();
await expect(page.getByTestId("welcome-banner")).toBeVisible(); await expect(page.getByTestId("welcome-banner")).toBeVisible();
await page await expect(page.getByTestId("welcome-banner")).toContainText("Start with creating a");
.getByTestId("welcome-banner")
.getByRole("button", { name: "Enable experimental features" })
.click();
// create loadpoint with charger // create loadpoint with charger
await expect(page.getByTestId("loadpoint-required")).toBeVisible();
await page.getByTestId("add-loadpoint").click(); await page.getByTestId("add-loadpoint").click();
const lpModal = page.getByTestId("loadpoint-modal"); const lpModal = page.getByTestId("loadpoint-modal");
await expectModalVisible(lpModal); await expectModalVisible(lpModal);
@ -63,8 +59,6 @@ test.describe("onboarding", async () => {
await lpModal.getByRole("button", { name: "Save" }).click(); await lpModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(lpModal); await expectModalHidden(lpModal);
await expect(page.getByTestId("welcome-banner")).not.toBeVisible(); await expect(page.getByTestId("welcome-banner")).not.toBeVisible();
await expect(page.getByTestId("loadpoint-required")).not.toBeVisible();
await expect(page.getByTestId("experimental-banner")).toBeVisible();
// create grid meter // create grid meter
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();

View file

@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import type { Page } from "@playwright/test"; import type { Page } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc"; import { start, stop, baseUrl } from "./evcc";
import { enableExperimental, expectModalVisible, getDatalistOptions } from "./utils"; import { expectModalVisible, getDatalistOptions } from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -23,7 +23,6 @@ test.afterAll(async () => {
async function openMeterModal(page: Page) { async function openMeterModal(page: Page) {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, true);
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();
const meterModal = page.getByTestId("meter-modal"); const meterModal = page.getByTestId("meter-modal");
await expectModalVisible(meterModal); await expectModalVisible(meterModal);

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { enableExperimental, expectModalHidden, expectModalVisible } from "./utils"; import { expectModalHidden, expectModalVisible } from "./utils";
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml"; const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
@ -16,7 +16,6 @@ test.afterAll(async () => {
test.describe("pv meter", async () => { test.describe("pv meter", async () => {
test("create, edit and remove pv meter", async ({ page }) => { test("create, edit and remove pv meter", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("pv")).toHaveCount(0); await expect(page.getByTestId("pv")).toHaveCount(0);
@ -73,7 +72,6 @@ test.describe("pv meter", async () => {
test("create broken pv meter with validation failure", async ({ page }) => { test("create broken pv meter with validation failure", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("pv")).toHaveCount(0); await expect(page.getByTestId("pv")).toHaveCount(0);

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden, enableExperimental } from "./utils"; import { expectModalVisible, expectModalHidden } from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -18,7 +18,6 @@ test.describe("SHM", () => {
test("configure SHM with validation and persistence", async ({ page }) => { test("configure SHM with validation and persistence", async ({ page }) => {
await start(CONFIG); await start(CONFIG);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
const shmCard = page.getByTestId("shm"); const shmCard = page.getByTestId("shm");

View file

@ -1,12 +1,6 @@
import { test, expect, type Page } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { import { expectModalHidden, expectModalVisible, editorClear, editorPaste } from "./utils";
enableExperimental,
expectModalHidden,
expectModalVisible,
editorClear,
editorPaste,
} from "./utils";
const CONFIG_WITH_TARIFFS = "config-with-tariffs.evcc.yaml"; const CONFIG_WITH_TARIFFS = "config-with-tariffs.evcc.yaml";
@ -17,25 +11,20 @@ test.afterEach(async () => {
await stop(); await stop();
}); });
async function goToConfig(page: Page) {
await page.goto("/#/config");
await enableExperimental(page);
}
test.describe("tariffs", async () => { test.describe("tariffs", async () => {
test("tariffs not configured", async ({ page }) => { test("tariffs not configured", async ({ page }) => {
await start(); await start();
await goToConfig(page); await page.goto("/#/config");
await expect(page.getByTestId("tariffs")).not.toBeVisible(); await expect(page.getByTestId("tariffs")).toBeVisible();
await expect(page.getByTestId("add-tariffs")).toBeVisible(); await expect(page.getByTestId("tariffs")).toContainText(["Configured", "no"].join(""));
}); });
test("tariffs via ui", async ({ page }) => { test("tariffs via ui", async ({ page }) => {
await start(); await start();
await goToConfig(page); await page.goto("/#/config");
await page.getByTestId("add-tariffs").click(); await page.getByTestId("tariffs").getByRole("button", { name: "edit" }).click();
const modal = await page.getByTestId("tariffs-modal"); const modal = await page.getByTestId("tariffs-modal");
await expectModalVisible(modal); await expectModalVisible(modal);
await page.waitForLoadState("networkidle"); await page.waitForLoadState("networkidle");
@ -86,7 +75,6 @@ grid:
test("tariffs from evcc.yaml", async ({ page }) => { test("tariffs from evcc.yaml", async ({ page }) => {
await start(CONFIG_WITH_TARIFFS); await start(CONFIG_WITH_TARIFFS);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("tariffs")).toBeVisible(); await expect(page.getByTestId("tariffs")).toBeVisible();
await expect(page.getByTestId("tariffs")).toContainText( await expect(page.getByTestId("tariffs")).toContainText(

View file

@ -1,12 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { import { editorClear, editorPaste, expectModalHidden, expectModalVisible } from "./utils";
editorClear,
editorPaste,
enableExperimental,
expectModalHidden,
expectModalVisible,
} from "./utils";
const CONFIG_WITH_VEHICLE = "config-with-vehicle.evcc.yaml"; const CONFIG_WITH_VEHICLE = "config-with-vehicle.evcc.yaml";
@ -24,7 +18,6 @@ test.describe("vehicles", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("vehicle")).toHaveCount(0); await expect(page.getByTestId("vehicle")).toHaveCount(0);
const vehicleModal = page.getByTestId("vehicle-modal"); const vehicleModal = page.getByTestId("vehicle-modal");
@ -83,7 +76,6 @@ test.describe("vehicles", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
await expect(page.getByTestId("vehicle")).toHaveCount(0); await expect(page.getByTestId("vehicle")).toHaveCount(0);
const vehicleModal = page.getByTestId("vehicle-modal"); const vehicleModal = page.getByTestId("vehicle-modal");
@ -120,7 +112,6 @@ test.describe("vehicles", async () => {
await start(CONFIG_WITH_VEHICLE); await start(CONFIG_WITH_VEHICLE);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("vehicle")).toHaveCount(1); await expect(page.getByTestId("vehicle")).toHaveCount(1);
const vehicleModal = page.getByTestId("vehicle-modal"); const vehicleModal = page.getByTestId("vehicle-modal");
@ -142,7 +133,6 @@ test.describe("vehicles", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
await page.getByTestId("add-vehicle").click(); await page.getByTestId("add-vehicle").click();
const vehicleModal = page.getByTestId("vehicle-modal"); const vehicleModal = page.getByTestId("vehicle-modal");
@ -181,7 +171,6 @@ test.describe("vehicles", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
await page.getByTestId("add-vehicle").click(); await page.getByTestId("add-vehicle").click();
const vehicleModal = page.getByTestId("vehicle-modal"); const vehicleModal = page.getByTestId("vehicle-modal");
@ -214,7 +203,6 @@ test.describe("vehicles", async () => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await page.getByTestId("add-vehicle").click(); await page.getByTestId("add-vehicle").click();
const modal = page.getByTestId("vehicle-modal"); const modal = page.getByTestId("vehicle-modal");

View file

@ -1,7 +1,6 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { import {
enableExperimental,
expectModalHidden, expectModalHidden,
expectModalVisible, expectModalVisible,
openTopNavigation, openTopNavigation,
@ -39,7 +38,6 @@ test.describe("general", async () => {
// change value in config // change value in config
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("generalconfig-title")).toContainText("Hello World"); await expect(page.getByTestId("generalconfig-title")).toContainText("Hello World");
await page.getByTestId("generalconfig-title").getByRole("button", { name: "edit" }).click(); await page.getByTestId("generalconfig-title").getByRole("button", { name: "edit" }).click();
@ -69,7 +67,6 @@ test.describe("general", async () => {
test.describe("network modal", async () => { test.describe("network modal", async () => {
test("persists host and external url across restart", async ({ page }) => { test("persists host and external url across restart", async ({ page }) => {
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page, false);
const networkEntry = page.getByTestId("generalconfig-network"); const networkEntry = page.getByTestId("generalconfig-network");
await expect(networkEntry).toBeVisible(); await expect(networkEntry).toBeVisible();

View file

@ -5,7 +5,6 @@ import {
expectModalHidden, expectModalHidden,
editorClear, editorClear,
editorPaste, editorPaste,
enableExperimental,
addDemoCharger, addDemoCharger,
newLoadpoint, newLoadpoint,
} from "./utils"; } from "./utils";
@ -32,7 +31,6 @@ test.describe("HEMS", () => {
test("grid sessions", async ({ page }) => { test("grid sessions", async ({ page }) => {
await start(CONFIG, "hems.sql"); await start(CONFIG, "hems.sql");
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
await page.getByTestId("hems").getByRole("button", { name: "edit" }).click(); await page.getByTestId("hems").getByRole("button", { name: "edit" }).click();
const hemsModal = page.getByTestId("hems-modal"); const hemsModal = page.getByTestId("hems-modal");
@ -56,7 +54,6 @@ test.describe("HEMS", () => {
test("modal yaml-configured", async ({ page }) => { test("modal yaml-configured", async ({ page }) => {
await start("hems-yaml.evcc.yaml"); await start("hems-yaml.evcc.yaml");
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
await page.getByTestId("hems").getByRole("button", { name: "edit" }).click(); await page.getByTestId("hems").getByRole("button", { name: "edit" }).click();
const hemsModal = page.getByTestId("hems-modal"); const hemsModal = page.getByTestId("hems-modal");
@ -76,7 +73,6 @@ test.describe("HEMS", () => {
await start(CONFIG); await start(CONFIG);
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// configure circuits // configure circuits
await page.getByTestId("circuits").getByRole("button", { name: "edit" }).click(); await page.getByTestId("circuits").getByRole("button", { name: "edit" }).click();

View file

@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc"; import { start, stop, restart, baseUrl } from "./evcc";
import { startSimulator, stopSimulator, simulatorHost } from "./simulator"; import { startSimulator, stopSimulator, simulatorHost } from "./simulator";
import { enableExperimental, expectModalVisible, expectModalHidden } from "./utils"; import { expectModalVisible, expectModalHidden } from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -53,9 +53,6 @@ test.describe("issue creation", () => {
await start(CONFIG); await start(CONFIG);
await page.goto("/#/config"); await page.goto("/#/config");
// Enable experimental features
await enableExperimental(page, false);
// Create a Shelly meter with username (to test private data redaction) // Create a Shelly meter with username (to test private data redaction)
await page.getByRole("button", { name: "Add grid meter" }).click(); await page.getByRole("button", { name: "Add grid meter" }).click();
const meterModal = page.getByTestId("meter-modal"); const meterModal = page.getByTestId("meter-modal");

View file

@ -1,6 +1,5 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc"; import { start, stop, baseUrl } from "./evcc";
import { enableExperimental } from "./utils";
test.use({ baseURL: baseUrl() }); test.use({ baseURL: baseUrl() });
@ -18,7 +17,6 @@ test.describe("sponsor token", () => {
test("token from YAML config", async ({ page }) => { test("token from YAML config", async ({ page }) => {
await start("sponsor.evcc.yaml"); await start("sponsor.evcc.yaml");
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// Check fatal error // Check fatal error
await expect(page.getByTestId("fatal-error")).toContainText("sponsorship"); await expect(page.getByTestId("fatal-error")).toContainText("sponsorship");
@ -40,7 +38,6 @@ test.describe("sponsor token", () => {
test("token from database config", async ({ page }) => { test("token from database config", async ({ page }) => {
await start(undefined, "sponsor.sql"); await start(undefined, "sponsor.sql");
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// Open sponsor modal // Open sponsor modal
await page await page
@ -61,7 +58,6 @@ test.describe("sponsor token", () => {
test("insert token in new installation", async ({ page }) => { test("insert token in new installation", async ({ page }) => {
await start(); await start();
await page.goto("/#/config"); await page.goto("/#/config");
await enableExperimental(page);
// Open sponsor modal and enter token // Open sponsor modal and enter token
await page await page

View file

@ -1,18 +1,15 @@
import { expect, type Page, type Locator } from "@playwright/test"; import { expect, type Page, type Locator } from "@playwright/test";
export async function enableExperimental(page: Page, inline = true): Promise<void> { export async function enableExperimental(page: Page): Promise<void> {
if (inline) { await page.goto("/#/config");
await page.getByRole("button", { name: "Enable Experimental Features" }).click();
} else { await page.getByTestId("generalconfig-experimental").click();
await openTopNavigation(page); const modal = page.getByTestId("experimental-modal");
await page.getByTestId("topnavigation-settings").click(); await expectModalVisible(modal);
const modal = page.getByTestId("global-settings-modal"); await modal.getByLabel("Show experimental UI features.").click();
await expectModalVisible(modal); await modal.getByRole("button", { name: "Close" }).click();
await modal.getByLabel("Experimental 🧪").click(); await expectModalHidden(modal);
await modal.getByRole("button", { name: "Close" }).click(); await expect(page.locator(".modal-backdrop")).not.toBeVisible();
await expectModalHidden(modal);
await expect(page.locator(".modal-backdrop")).not.toBeVisible();
}
} }
export async function openTopNavigation(page: Page): Promise<void> { export async function openTopNavigation(page: Page): Promise<void> {