Energyflow UI: expand solar/battery (#20389)

This commit is contained in:
Michael Geers 2025-04-04 18:16:54 +02:00 • committed by GitHub
parent 667276b450
commit dc57999c95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 161 additions and 64 deletions

View file

@ -82,7 +82,6 @@
:name="$t('main.energyflow.pvProduction')"
icon="sun"
:power="pvProduction"
:powerTooltip="pvTooltip"
:details="solarForecastRemainingToday"
:detailsFmt="forecastFmt"
:detailsTooltip="solarForecastTooltip"
@ -90,15 +89,27 @@
:detailsIcon="solarForecastIcon"
:detailsClickable="solarForecastExists"
:powerUnit="powerUnit"
:expanded="pvExpanded"
data-testid="energyflow-entry-production"
@details-clicked="openForecastModal"
/>
@toggle="togglePv"
>
<template v-if="pv.length > 1" #expanded>
<EnergyflowEntry
v-for="(p, index) in pv"
:key="index"
:name="p.title || genericPvTitle(index)"
:power="p.power"
:powerUnit="powerUnit"
:data-testid="`energyflow-entry-production-${index}`"
/>
</template>
</EnergyflowEntry>
<EnergyflowEntry
v-if="batteryConfigured"
:name="batteryDischargeLabel"
icon="battery"
:power="batteryDischarge"
:powerTooltip="batteryDischargeTooltip"
:powerUnit="powerUnit"
:iconProps="{
hold: batteryHold,
@ -107,13 +118,26 @@
}"
:details="batterySoc"
:detailsFmt="batteryFmt"
:expanded="batteryExpanded"
detailsClickable
data-testid="energyflow-entry-batterydischarge"
@details-clicked="openBatterySettingsModal"
@toggle="toggleBattery"
>
<template v-if="batteryGridChargeLimitSet" #subline>
<div class="d-none d-md-block">&nbsp;</div>
</template>
<template v-if="battery.length > 1" #expanded>
<EnergyflowEntry
v-for="(b, index) in battery"
:key="index"
:name="b.title || genericBatteryTitle(index)"
:details="b.soc"
:detailsFmt="batteryFmt"
:power="dischargePower(b.power)"
:powerUnit="powerUnit"
/>
</template>
</EnergyflowEntry>
<EnergyflowEntry
:name="$t('main.energyflow.gridImport')"
@ -174,7 +198,6 @@
:name="batteryChargeLabel"
icon="battery"
:power="batteryCharge"
:powerTooltip="batteryChargeTooltip"
:powerUnit="powerUnit"
:iconProps="{
hold: batteryHold,
@ -183,8 +206,10 @@
}"
:details="batterySoc"
:detailsFmt="batteryFmt"
:expanded="batteryExpanded"
detailsClickable
@details-clicked="openBatterySettingsModal"
@toggle="toggleBattery"
>
<template v-if="batteryGridChargeLimitSet" #subline>
<button
@ -207,6 +232,17 @@
</span>
</button>
</template>
<template v-if="battery.length > 1" #expanded>
<EnergyflowEntry
v-for="(b, index) in battery"
:key="index"
:name="b.title || genericBatteryTitle(index)"
:details="b.soc"
:detailsFmt="batteryFmt"
:power="chargePower(b.power)"
:powerUnit="powerUnit"
/>
</template>
</EnergyflowEntry>
<EnergyflowEntry
v-if="pvPossible"
@ -348,21 +384,6 @@ export default {
detailsHeight() {
return this.detailsOpen ? this.detailsCompleteHeight + "px" : 0;
},
pvTooltip() {
if (!Array.isArray(this.pv) || this.pv.length <= 1) {
return;
}
return this.pv.map(
({ power, title }) =>
`${title ? `${title}: ` : ""}${this.fmtW(power, this.powerUnit)}`
);
},
batteryDischargeTooltip() {
return this.batteryTooltip(true);
},
batteryChargeTooltip() {
return this.batteryTooltip(false);
},
batteryFmt() {
return (soc) => this.fmtPercentage(soc, 0);
},
@ -412,6 +433,12 @@ export default {
}
return [];
},
pvExpanded() {
return settings.energyflowPv;
},
batteryExpanded() {
return settings.energyflowBattery;
},
},
watch: {
pvConfigured() {
@ -494,17 +521,19 @@ export default {
chargePower(power) {
return Math.abs(Math.min(0, power) * -1);
},
batteryTooltip(discharge = false) {
if (!Array.isArray(this.battery) || this.battery.length <= 1) {
return;
}
return this.battery.map(({ power, soc, title }) => {
const value = discharge ? this.dischargePower(power) : this.chargePower(power);
const powerFmt = this.fmtW(value, this.powerUnit);
const socFmt = this.fmtPercentage(soc, 0);
return `${title ? `${title}: ` : ""}${powerFmt} (${socFmt})`;
});
toggleBattery() {
settings.energyflowBattery = !settings.energyflowBattery;
this.$nextTick(this.updateHeight);
},
togglePv() {
settings.energyflowPv = !settings.energyflowPv;
this.$nextTick(this.updateHeight);
},
genericBatteryTitle(index) {
return `${this.$t("config.devices.batteryStorage")} #${index + 1}`;
},
genericPvTitle(index) {
return `${this.$t("config.devices.solarSystem")} #${index + 1}`;
},
},
};

View file

@ -1,39 +1,63 @@
<template>
<div class="mb-2 entry" :class="{ 'evcc-gray': !active }">
<div class="d-flex justify-content-between">
<span class="d-flex flex-nowrap">
<BatteryIcon v-if="isBattery" v-bind="iconProps" />
<VehicleIcon v-else-if="isVehicle" v-bind="iconProps" />
<component :is="`shopicon-regular-${icon}`" v-else></component>
</span>
<div class="d-block text-nowrap flex-grow-1 ms-3 text-truncate">
{{ name }}
<div>
<div class="mb-2 entry" :class="{ 'evcc-gray': !active }">
<div class="d-flex justify-content-between">
<span class="d-flex flex-nowrap">
<BatteryIcon v-if="isBattery" v-bind="iconProps" />
<VehicleIcon v-else-if="isVehicle" v-bind="iconProps" />
<div v-else-if="!icon" class="icon-placeholder"></div>
<component :is="`shopicon-regular-${icon}`" v-else></component>
</span>
<div class="d-block flex-grow-1 ms-3 text-truncate">
<span v-if="!$slots.expanded">{{ name }}</span>
<button
v-else
class="btn-neutral d-flex align-items-baseline flex-shrink-1 flex-grow-1"
style="max-width: 100%"
@click="toggle"
>
<div class="text-truncate flex-shrink-1 flex-grow-0">
{{ name }}
</div>
<shopicon-regular-arrowdropdown
class="expand-icon flex-shrink-0 flex-grow-0"
:class="{ 'expand-icon--expanded': expanded }"
/>
</button>
</div>
<span class="text-end text-nowrap ps-1 fw-bold d-flex">
<div
ref="details"
class="fw-normal"
:class="{
'text-decoration-underline': detailsClickable,
'evcc-gray': detailsInactive,
}"
data-testid="energyflow-entry-details"
data-bs-toggle="tooltip"
:tabindex="detailsClickable ? 0 : undefined"
@click="detailsClicked"
>
<ForecastIcon
v-if="detailsIcon === 'forecast'"
class="ms-2 me-1 d-inline-block"
/>
<AnimatedNumber v-if="!isNaN(details)" :to="details" :format="detailsFmt" />
</div>
<div ref="power" class="power" data-bs-toggle="tooltip" @click="powerClicked">
<AnimatedNumber ref="powerNumber" :to="power" :format="kw" />
</div>
</span>
</div>
<span class="text-end text-nowrap ps-1 fw-bold d-flex">
<div
ref="details"
class="fw-normal"
:class="{
'text-decoration-underline': detailsClickable,
'evcc-gray': detailsInactive,
}"
data-testid="energyflow-entry-details"
data-bs-toggle="tooltip"
:tabindex="detailsClickable ? 0 : undefined"
@click="detailsClicked"
>
<ForecastIcon
v-if="detailsIcon === 'forecast'"
class="ms-2 me-1 d-inline-block"
/>
<AnimatedNumber v-if="!isNaN(details)" :to="details" :format="detailsFmt" />
</div>
<div ref="power" class="power" data-bs-toggle="tooltip" @click="powerClicked">
<AnimatedNumber ref="powerNumber" :to="power" :format="kw" />
</div>
</span>
</div>
<div v-if="$slots.subline" class="ms-4 ps-3">
<div
v-if="$slots.expanded"
class="expandable ms-2"
:class="{ 'expandable--open': expanded }"
>
<slot name="expanded" />
</div>
<div v-if="$slots.subline" class="ms-4 ps-3 mb-2">
<slot name="subline" />
</div>
</div>
@ -43,6 +67,7 @@
import "@h2d2/shopicons/es/regular/powersupply";
import "@h2d2/shopicons/es/regular/sun";
import "@h2d2/shopicons/es/regular/home";
import "@h2d2/shopicons/es/regular/arrowdropdown";
import Tooltip from "bootstrap/js/dist/tooltip";
import BatteryIcon from "./BatteryIcon.vue";
import formatter from "../../mixins/formatter";
@ -67,8 +92,9 @@ export default {
detailsTooltip: { type: Array },
detailsClickable: { type: Boolean },
detailsInactive: { type: Boolean },
expanded: { type: Boolean, default: false },
},
emits: ["details-clicked"],
emits: ["details-clicked", "toggle"],
data() {
return { powerTooltipInstance: null, detailsTooltipInstance: null };
},
@ -153,6 +179,10 @@ export default {
// hide tooltip, chrome needs a timeout
setTimeout(() => this.detailsTooltipInstance?.hide(), 10);
},
toggle($event) {
$event.stopPropagation();
this.$emit("toggle");
},
},
};
</script>
@ -163,4 +193,25 @@ export default {
.power {
min-width: 75px;
}
.icon-placeholder {
width: 24px;
aspect-ratio: 1;
}
.expand-icon {
transition: transform var(--evcc-transition-medium) ease;
transform: rotate(-90deg);
}
.expand-icon--expanded {
transform: rotate(0deg);
}
.expandable {
overflow: hidden;
opacity: 0;
height: 0;
transition: opacity var(--evcc-transition-medium) ease-in;
}
.expandable--open {
opacity: 1;
height: auto;
}
</style>

View file

@ -5,6 +5,8 @@ const SETTINGS_THEME = "settings_theme";
const SETTINGS_UNIT = "settings_unit";
const SETTINGS_HIDDEN_FEATURES = "settings_hidden_features";
const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
const SETTINGS_ENERGYFLOW_PV = "settings_energyflow_pv";
const SETTINGS_ENERGYFLOW_BATTERY = "settings_energyflow_battery";
const SESSION_INFO = "session_info";
const SESSION_COLUMNS = "session_columns";
const SAVINGS_PERIOD = "savings_period";
@ -58,6 +60,8 @@ const settings = reactive({
unit: read(SETTINGS_UNIT),
hiddenFeatures: readBool(SETTINGS_HIDDEN_FEATURES),
energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV),
energyflowBattery: readBool(SETTINGS_ENERGYFLOW_BATTERY),
sessionInfo: readArray(SESSION_INFO),
sessionColumns: readArray(SESSION_COLUMNS),
savingsPeriod: read(SAVINGS_PERIOD),
@ -72,6 +76,8 @@ watch(() => settings.theme, save(SETTINGS_THEME));
watch(() => settings.unit, save(SETTINGS_UNIT));
watch(() => settings.hiddenFeatures, saveBool(SETTINGS_HIDDEN_FEATURES));
watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV));
watch(() => settings.energyflowBattery, saveBool(SETTINGS_ENERGYFLOW_BATTERY));
watch(() => settings.sessionInfo, saveArray(SESSION_INFO));
watch(() => settings.sessionColumns, saveArray(SESSION_COLUMNS));
watch(() => settings.savingsPeriod, save(SAVINGS_PERIOD));

View file

@ -108,6 +108,11 @@ A = "A (nicht verbunden)"
B = "B (verbunden)"
C = "C (lädt)"
[config.devices]
auxMeter = "Smarter Verbraucher"
batteryStorage = "Batterie"
solarSystem = "PV-Anlage"
[config.eebus]
description = "Grundkonfiguration für die Kommunikation mit anderen EEBus-Geräten."
title = "EEBus"

View file

@ -108,6 +108,11 @@ A = "A (not connected)"
B = "B (connected)"
C = "C (charging)"
[config.devices]
auxMeter = "Smart consumer"
batteryStorage = "Battery storage"
solarSystem = "Solar system"
[config.eebus]
description = "Configuration that enables evcc to communicate with other EEBus devices."
title = "EEBus"
@ -591,6 +596,7 @@ gridImport = "Grid use"
homePower = "Consumption"
loadpoints = "Charger| Charger | {count} chargers"
noEnergy = "No meter data"
pv = "Solar system"
pvExport = "Grid export"
pvProduction = "Production"
selfConsumption = "Self-consumption"