Energyflow UI: add price/co2 toggle (#25717)

This commit is contained in:
Michael Geers 2025-12-01 16:52:42 +01:00 • committed by GitHub
parent bdcacb8374
commit 25382accc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 84 additions and 28 deletions

View file

@ -140,14 +140,16 @@
</template> </template>
</EnergyflowEntry> </EnergyflowEntry>
<EnergyflowEntry <EnergyflowEntry
:key="`grid-${showCo2}`"
:name="$t('main.energyflow.gridImport')" :name="$t('main.energyflow.gridImport')"
icon="powersupply" icon="powersupply"
:power="gridImport" :power="gridImport"
:powerUnit="powerUnit" :powerUnit="powerUnit"
:details="detailsValue(tariffGrid, tariffCo2)" :details="detailsValue(tariffGrid, tariffCo2)"
:detailsFmt="detailsFmt" :detailsFmt="detailsFmt"
:detailsTooltip="detailsTooltip(tariffGrid, tariffCo2)" :detailsClickable="hasPriceAndCo2"
data-testid="energyflow-entry-gridimport" data-testid="energyflow-entry-gridimport"
@details-clicked="toggleCo2"
/> />
</div> </div>
</div> </div>
@ -163,15 +165,17 @@
<div> <div>
<EnergyflowEntry <EnergyflowEntry
v-if="pvPossible" v-if="pvPossible"
:key="`home-${showCo2}`"
:name="$t('main.energyflow.homePower')" :name="$t('main.energyflow.homePower')"
icon="home" icon="home"
:power="homePower" :power="homePower"
:powerUnit="powerUnit" :powerUnit="powerUnit"
:details="detailsValue(tariffPriceHome, tariffCo2Home)" :details="detailsValue(tariffPriceHome, tariffCo2Home)"
:detailsFmt="detailsFmt" :detailsFmt="detailsFmt"
:detailsTooltip="detailsTooltip(tariffPriceHome, tariffCo2Home)" :detailsClickable="hasPriceAndCo2"
data-testid="energyflow-entry-home" data-testid="energyflow-entry-home"
:expanded="consumersExpanded" :expanded="consumersExpanded"
@details-clicked="toggleCo2"
@toggle="toggleConsumers" @toggle="toggleConsumers"
> >
<template v-if="consumers.length > 0" #expanded> <template v-if="consumers.length > 0" #expanded>
@ -188,6 +192,7 @@
</template> </template>
</EnergyflowEntry> </EnergyflowEntry>
<EnergyflowEntry <EnergyflowEntry
:key="`loadpoints-${showCo2}`"
:name="loadpointsLabel" :name="loadpointsLabel"
icon="vehicle" icon="vehicle"
:iconProps="{ names: vehicleIcons }" :iconProps="{ names: vehicleIcons }"
@ -199,11 +204,10 @@
: undefined : undefined
" "
:detailsFmt="detailsFmt" :detailsFmt="detailsFmt"
:detailsTooltip=" :detailsClickable="hasPriceAndCo2"
detailsTooltip(tariffPriceLoadpoints, tariffCo2Loadpoints)
"
data-testid="energyflow-entry-loadpoints" data-testid="energyflow-entry-loadpoints"
:expanded="loadpointsExpanded" :expanded="loadpointsExpanded"
@details-clicked="toggleCo2"
@toggle="toggleLoadpoints" @toggle="toggleLoadpoints"
> >
<template v-if="activeLoadpointsCount > 0" #expanded> <template v-if="activeLoadpointsCount > 0" #expanded>
@ -277,14 +281,16 @@
</EnergyflowEntry> </EnergyflowEntry>
<EnergyflowEntry <EnergyflowEntry
v-if="pvPossible" v-if="pvPossible"
:key="`export-${showCo2}`"
:name="$t('main.energyflow.pvExport')" :name="$t('main.energyflow.pvExport')"
icon="powersupply" icon="powersupply"
:power="pvExport" :power="pvExport"
:powerUnit="powerUnit" :powerUnit="powerUnit"
:details="detailsValue(-tariffFeedIn)" :details="detailsValue(-tariffFeedIn)"
:detailsFmt="detailsFmt" :detailsFmt="detailsFmt"
:detailsTooltip="detailsTooltip(-tariffFeedIn)" :detailsClickable="hasPriceAndCo2"
data-testid="energyflow-entry-gridexport" data-testid="energyflow-entry-gridexport"
@details-clicked="toggleCo2"
/> />
</div> </div>
</div> </div>
@ -356,6 +362,21 @@ export default defineComponent({
return { detailsOpen: false, detailsCompleteHeight: null as number | null, ready: false }; return { detailsOpen: false, detailsCompleteHeight: null as number | null, ready: false };
}, },
computed: { computed: {
showCo2() {
if (this.hasCo2 && !this.hasPrice) {
return true;
}
return settings.energyflowCo2;
},
hasPrice() {
return this.tariffGrid !== undefined;
},
hasCo2() {
return this.tariffCo2 !== undefined;
},
hasPriceAndCo2() {
return this.hasPrice && this.hasCo2;
},
gridImport() { gridImport() {
return Math.max(0, this.gridPower); return Math.max(0, this.gridPower);
}, },
@ -434,14 +455,14 @@ export default defineComponent({
fmtLoadpointTemp() { fmtLoadpointTemp() {
return (temp: number) => this.fmtTemperature(temp); return (temp: number) => this.fmtTemperature(temp);
}, },
co2Available() { smartCostCo2() {
return this.smartCostType === SMART_COST_TYPE.CO2; return this.smartCostType === SMART_COST_TYPE.CO2;
}, },
pvPossible() { pvPossible() {
return this.pvConfigured || this.gridConfigured; return this.pvConfigured || this.gridConfigured;
}, },
batteryGridChargeNow() { batteryGridChargeNow() {
if (this.co2Available) { if (this.smartCostCo2) {
return this.fmtCo2Short(this.tariffCo2); return this.fmtCo2Short(this.tariffCo2);
} }
return this.fmtPricePerKWh(this.tariffGrid, this.currency, true); return this.fmtPricePerKWh(this.tariffGrid, this.currency, true);
@ -455,7 +476,7 @@ export default defineComponent({
if (!this.batteryGridChargeLimitSet) { if (!this.batteryGridChargeLimitSet) {
return; return;
} }
if (this.co2Available) { if (this.smartCostCo2) {
return this.fmtCo2Short(this.batteryGridChargeLimit); return this.fmtCo2Short(this.batteryGridChargeLimit);
} }
return this.fmtPricePerKWh(this.batteryGridChargeLimit, this.currency, true); return this.fmtPricePerKWh(this.batteryGridChargeLimit, this.currency, true);
@ -532,27 +553,16 @@ export default defineComponent({
window.removeEventListener("resize", this.updateHeight); window.removeEventListener("resize", this.updateHeight);
}, },
methods: { methods: {
detailsTooltip(price?: number, co2?: number) {
const result = [];
if (co2 !== undefined) {
result.push(`${this.fmtCo2Long(co2)}`);
}
if (price !== undefined) {
result.push(`${this.fmtPricePerKWh(price, this.currency)}`);
}
return result;
},
detailsValue(price?: number, co2?: number) { detailsValue(price?: number, co2?: number) {
if (this.co2Available) { return this.showCo2 ? co2 : price;
return co2;
}
return price;
}, },
detailsFmt(value: number) { detailsFmt(value: number) {
if (this.co2Available) { return this.showCo2
return this.fmtCo2Short(value); ? this.fmtCo2Short(value)
} : this.fmtPricePerKWh(value, this.currency, true);
return this.fmtPricePerKWh(value, this.currency, true); },
toggleCo2() {
settings.energyflowCo2 = !settings.energyflowCo2;
}, },
forecastFmt(value: number) { forecastFmt(value: number) {
if (typeof value !== "number") return ""; if (typeof value !== "number") return "";

View file

@ -30,7 +30,7 @@
<span class="text-end text-nowrap ps-1 fw-bold d-flex align-items-center"> <span class="text-end text-nowrap ps-1 fw-bold d-flex align-items-center">
<div <div
ref="details" ref="details"
class="fw-normal d-flex align-items-center" class="fw-normal d-flex align-items-center user-select-none"
:class="{ :class="{
'text-decoration-underline': detailsClickable, 'text-decoration-underline': detailsClickable,
'evcc-gray': detailsInactive, 'evcc-gray': detailsInactive,

View file

@ -8,6 +8,7 @@ const SETTINGS_UNIT = "settings_unit";
const SETTINGS_12H_FORMAT = "settings_12h_format"; const SETTINGS_12H_FORMAT = "settings_12h_format";
const SETTINGS_HIDDEN_FEATURES = "settings_hidden_features"; const SETTINGS_HIDDEN_FEATURES = "settings_hidden_features";
const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details"; const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
const SETTINGS_ENERGYFLOW_CO2 = "settings_energyflow_co2";
const SETTINGS_ENERGYFLOW_PV = "settings_energyflow_pv"; const SETTINGS_ENERGYFLOW_PV = "settings_energyflow_pv";
const SETTINGS_ENERGYFLOW_BATTERY = "settings_energyflow_battery"; const SETTINGS_ENERGYFLOW_BATTERY = "settings_energyflow_battery";
const SETTINGS_ENERGYFLOW_LOADPOINTS = "settings_energyflow_loadpoints"; const SETTINGS_ENERGYFLOW_LOADPOINTS = "settings_energyflow_loadpoints";
@ -105,6 +106,7 @@ export interface Settings {
is12hFormat: boolean; is12hFormat: boolean;
hiddenFeatures: boolean; hiddenFeatures: boolean;
energyflowDetails: boolean; energyflowDetails: boolean;
energyflowCo2: boolean;
energyflowPv: boolean; energyflowPv: boolean;
energyflowBattery: boolean; energyflowBattery: boolean;
energyflowLoadpoints: boolean; energyflowLoadpoints: boolean;
@ -126,6 +128,7 @@ const settings: Settings = reactive({
is12hFormat: readBool(SETTINGS_12H_FORMAT), is12hFormat: readBool(SETTINGS_12H_FORMAT),
hiddenFeatures: readBool(SETTINGS_HIDDEN_FEATURES), hiddenFeatures: readBool(SETTINGS_HIDDEN_FEATURES),
energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS), energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
energyflowCo2: readBool(SETTINGS_ENERGYFLOW_CO2),
energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV), energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV),
energyflowBattery: readBool(SETTINGS_ENERGYFLOW_BATTERY), energyflowBattery: readBool(SETTINGS_ENERGYFLOW_BATTERY),
energyflowLoadpoints: readBool(SETTINGS_ENERGYFLOW_LOADPOINTS), energyflowLoadpoints: readBool(SETTINGS_ENERGYFLOW_LOADPOINTS),
@ -146,6 +149,7 @@ watch(() => settings.unit, save(SETTINGS_UNIT));
watch(() => settings.is12hFormat, saveBool(SETTINGS_12H_FORMAT)); watch(() => settings.is12hFormat, saveBool(SETTINGS_12H_FORMAT));
watch(() => settings.hiddenFeatures, saveBool(SETTINGS_HIDDEN_FEATURES)); watch(() => settings.hiddenFeatures, saveBool(SETTINGS_HIDDEN_FEATURES));
watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS)); watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
watch(() => settings.energyflowCo2, saveBool(SETTINGS_ENERGYFLOW_CO2));
watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV)); watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV));
watch(() => settings.energyflowBattery, saveBool(SETTINGS_ENERGYFLOW_BATTERY)); watch(() => settings.energyflowBattery, saveBool(SETTINGS_ENERGYFLOW_BATTERY));
watch(() => settings.energyflowLoadpoints, saveBool(SETTINGS_ENERGYFLOW_LOADPOINTS)); watch(() => settings.energyflowLoadpoints, saveBool(SETTINGS_ENERGYFLOW_LOADPOINTS));

42
tests/energyflow.spec.ts Normal file
View file

@ -0,0 +1,42 @@
import { test, expect } from "@playwright/test";
import { start, stop, baseUrl } from "./evcc";
const CONFIG = "config-with-tariffs.evcc.yaml";
test.use({ baseURL: baseUrl() });
test.beforeAll(async () => {
await start(CONFIG);
});
test.afterAll(async () => {
await stop();
});
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("tariff toggle", async ({ page }) => {
const energyflow = page.getByTestId("energyflow");
await energyflow.click();
const details = page
.getByTestId("energyflow-entry-gridimport")
.getByTestId("energyflow-entry-details");
// check price
await expect(details).toHaveText("30.0 öre");
// toggle to co2
await details.click();
await expect(details).toHaveText("300 g");
// reload page and verify persistence
await page.reload();
await expect(details).toHaveText("300 g");
// toggle back to price
await details.click();
await expect(details).toHaveText("30.0 öre");
});