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>
<div
class="root round-box"
:class="{ 'round-box--error': error, 'round-box--warning': warning }"
class="root"
: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="icon me-2">
@ -16,7 +21,7 @@
<button
ref="tooltip"
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 }"
data-bs-toggle="tooltip"
data-bs-html="true"
@ -45,6 +50,7 @@ export default {
title: String,
editable: Boolean,
error: Boolean,
unconfigured: Boolean,
warning: Boolean,
noEditButton: Boolean,
},
@ -101,9 +107,26 @@ export default {
padding: 1rem 1.5rem;
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 {
display: none;
}
.edit-button {
/* transparent button, right align icon */
margin-right: -0.5rem;
}
.divide {
margin-left: -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"
:label="$t('config.general.experimental')"
:text="$t(`config.general.${hiddenFeatures ? 'on' : 'off'}`)"
modal-id="globalSettingsModal"
modal-id="experimentalModal"
/>
<GeneralConfigEntry
v-if="$hiddenFeatures()"
test-id="generalconfig-sponsoring"
:label="$t('config.sponsor.title')"
:text="sponsorStatus.title"
:text-class="sponsorStatus.textClass"
modal-id="sponsorModal"
experimental
>
<template #text-prefix>
<span
@ -48,21 +46,17 @@
</GeneralConfigEntry>
<GeneralConfigEntry
v-if="$hiddenFeatures()"
test-id="generalconfig-network"
:label="$t('config.network.title')"
:text="networkStatus"
modal-id="networkModal"
experimental
/>
<GeneralConfigEntry
v-if="$hiddenFeatures()"
test-id="generalconfig-control"
:label="$t('config.control.title')"
:text="controlStatus"
modal-id="controlModal"
experimental
/>
<TitleModal ref="titleModal" @changed="load" />
</div>

View file

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

View file

@ -1,75 +1,15 @@
<template>
<div class="alert alert-success" data-testid="welcome-banner">
<h5 class="alert-heading">Welcome to evcc!</h5>
<p class="mb-4">Ready to get started? Choose how you'd like to configure your devices:</p>
<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>
<h5 class="alert-heading">{{ $t("config.main.welcomeBannerTitle") }}</h5>
<Markdown :markdown="$t('config.main.welcomeBannerText')" class="mb-0" />
</div>
</template>
<script>
import { setHiddenFeatures, getHiddenFeatures } from "@/featureflags.ts";
import Markdown from "./Markdown.vue";
export default {
name: "WelcomeBanner",
computed: {
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();
},
},
components: { Markdown },
};
</script>

View file

@ -74,22 +74,6 @@
{{ $t("settings.fullscreen.enter") }}
</button>
</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">
{{ $t("settings.deviceInfo") }}
</div>
@ -108,7 +92,6 @@ import {
} from "@/i18n.ts";
import { getThemePreference, setThemePreference } from "@/theme.ts";
import { getUnits, setUnits, is12hFormat, set12hFormat } from "@/units";
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags.ts";
import { isApp } from "@/utils/native";
import { defineComponent, type PropType } from "vue";
import { LENGTH_UNIT, THEME, type UiLoadpoint } from "@/types/evcc";
@ -128,7 +111,6 @@ export default defineComponent({
language: getLocalePreference() || "",
unit: getUnits(),
timeFormat: is12hFormat() ? TIME_12H : TIME_24H,
hiddenFeatures: getHiddenFeatures(),
fullscreenActive: false,
THEMES: Object.values(THEME),
UNITS: Object.values(LENGTH_UNIT),
@ -162,9 +144,6 @@ export default defineComponent({
theme(value) {
setThemePreference(value);
},
hiddenFeatures(value) {
setHiddenFeatures(value);
},
language(value) {
const i18n = this.$root?.$i18n;
if (!i18n) return;

View file

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

View file

@ -6,7 +6,7 @@
<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">
{{ $t("main.loadpointSettings.batteryBoost.label") }}&nbsp;🧪
{{ $t("main.loadpointSettings.batteryBoost.label") }}
</label>
<div class="col-sm-8 pe-0">
<div class="form-check form-switch my-1">

View file

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

View file

@ -175,6 +175,10 @@
"description": "Grundkonfiguration für die Kommunikation mit anderen EEBus-Geräten.",
"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": {
"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",
@ -383,6 +387,8 @@
"title": "Konfiguration",
"unconfigured": "nicht konfiguriert",
"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."
},
"messaging": {
@ -1201,7 +1207,7 @@
},
"hiddenFeatures": {
"label": "Experimentell",
"value": "Experimentelle UI-Funktionen zeigen."
"value": "Experimentelle Funktionen anzeigen."
},
"language": {
"auto": "Automatisch",

View file

@ -175,6 +175,10 @@
"description": "Configuration that enables evcc to communicate with other EEBus devices.",
"title": "EEBus"
},
"experimental": {
"description": "Enable user interface for features that are still being tested and may change in future versions.",
"title": "Experimental"
},
"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).",
"titleAdd": "Add Consumer Meter",
@ -383,6 +387,8 @@
"title": "Configuration",
"unconfigured": "not configured",
"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."
},
"messaging": {
@ -1201,7 +1207,7 @@
},
"hiddenFeatures": {
"label": "Experimental",
"value": "Show experimental UI features."
"value": "Show experimental features."
},
"language": {
"auto": "Automatic",

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
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";
@ -16,7 +16,6 @@ test.afterAll(async () => {
test.describe("battery meter", async () => {
test("create, edit and remove battery meter", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("battery")).toHaveCount(0);
@ -69,7 +68,6 @@ test.describe("battery meter", async () => {
test("advanced fields", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page, false);
await page.getByRole("button", { name: "Add solar or battery" }).click();

View file

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

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { enableExperimental, expectModalVisible, expectModalHidden } from "./utils";
import { expectModalVisible, expectModalHidden } from "./utils";
import { simulatorUrl, startSimulator, stopSimulator } from "./simulator";
test.use({ baseURL: baseUrl() });
@ -30,7 +30,6 @@ test.afterEach(async () => {
test.describe("config device auth", async () => {
test("create grid meter with redirect auth", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page, true);
// verify no grid meter exists yet
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 }) => {
await page.goto("/#/config");
await enableExperimental(page, true);
// create a grid meter with device-code auth
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 }) => {
await page.goto("/#/config");
await enableExperimental(page, true);
await page.getByRole("button", { name: "Add grid meter" }).click();
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 }) => {
await page.goto("/#/config");
await enableExperimental(page, true);
// create a grid meter with auth
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 }) => {
await page.goto("/#/config");
await enableExperimental(page, true);
// create a grid meter with auth and prepare connection
await page.getByRole("button", { name: "Add grid meter" }).click();

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
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";
@ -15,7 +15,6 @@ test.describe("invalid template migration", async () => {
await start(undefined, CONFIG_INVALID_TEMPLATE);
await page.goto("/#/config");
await enableExperimental(page, false);
// startup error
await expect(page.getByTestId("fatal-error")).toBeVisible();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
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";
@ -16,7 +16,6 @@ test.afterAll(async () => {
test.describe("pv meter", async () => {
test("create, edit and remove pv meter", async ({ page }) => {
await page.goto("/#/config");
await enableExperimental(page, false);
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 }) => {
await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("pv")).toHaveCount(0);

View file

@ -1,6 +1,6 @@
import { test, expect } from "@playwright/test";
import { start, stop, restart, baseUrl } from "./evcc";
import { expectModalVisible, expectModalHidden, enableExperimental } from "./utils";
import { expectModalVisible, expectModalHidden } from "./utils";
test.use({ baseURL: baseUrl() });
@ -18,7 +18,6 @@ test.describe("SHM", () => {
test("configure SHM with validation and persistence", async ({ page }) => {
await start(CONFIG);
await page.goto("/#/config");
await enableExperimental(page, false);
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 {
enableExperimental,
expectModalHidden,
expectModalVisible,
editorClear,
editorPaste,
} from "./utils";
import { expectModalHidden, expectModalVisible, editorClear, editorPaste } from "./utils";
const CONFIG_WITH_TARIFFS = "config-with-tariffs.evcc.yaml";
@ -17,25 +11,20 @@ test.afterEach(async () => {
await stop();
});
async function goToConfig(page: Page) {
await page.goto("/#/config");
await enableExperimental(page);
}
test.describe("tariffs", async () => {
test("tariffs not configured", async ({ page }) => {
await start();
await goToConfig(page);
await page.goto("/#/config");
await expect(page.getByTestId("tariffs")).not.toBeVisible();
await expect(page.getByTestId("add-tariffs")).toBeVisible();
await expect(page.getByTestId("tariffs")).toBeVisible();
await expect(page.getByTestId("tariffs")).toContainText(["Configured", "no"].join(""));
});
test("tariffs via ui", async ({ page }) => {
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");
await expectModalVisible(modal);
await page.waitForLoadState("networkidle");
@ -86,7 +75,6 @@ grid:
test("tariffs from evcc.yaml", async ({ page }) => {
await start(CONFIG_WITH_TARIFFS);
await page.goto("/#/config");
await enableExperimental(page, false);
await expect(page.getByTestId("tariffs")).toBeVisible();
await expect(page.getByTestId("tariffs")).toContainText(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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