Improve session info discoverability (#9783)

This commit is contained in:
Michael Geers 2023-09-09 10:01:23 +02:00 • committed by GitHub
parent 6f7e45db41
commit 5ef8825582
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 216 additions and 126 deletions

View file

@ -1,13 +1,17 @@
<template>
<div class="root">
<div class="mb-2 label" :class="labelClass">{{ label }}</div>
<div class="mb-2 label" :class="labelClass">
<slot name="label">{{ label }}</slot>
</div>
<slot>
<h3 class="value m-0 d-block d-sm-flex align-items-baseline" :class="valueClass">
<AnimatedNumber v-if="valueFmt" :to="value" :format="valueFmt" />
<span v-else>{{ value }}</span>
<div v-if="extraValue" class="extraValue ms-0 ms-sm-1 text-nowrap">
{{ extraValue }}
</div>
<slot name="value">
<AnimatedNumber v-if="valueFmt" :to="value" :format="valueFmt" />
<span v-else>{{ value }}</span>
<div v-if="extraValue" class="extraValue ms-0 ms-sm-1 text-nowrap">
{{ extraValue }}
</div>
</slot>
</h3>
</slot>
</div>

View file

@ -78,66 +78,7 @@
:value="fmtEnergy(chargedEnergy)"
align="center"
/>
<div class="sessionInfo" @click="nextSessionInfo">
<LabelAndValue
v-if="sessionInfo === SESSION.SOLAR"
:label="$t('main.loadpoint.solar')"
:value="sessionSolarPercentage"
:valueFmt="fmtSolar"
align="end"
/>
<div v-else-if="sessionInfo === SESSION.AVG_PRICE">
<LabelAndValue
class="d-block d-sm-none"
:label="$t('main.loadpoint.avgPrice')"
:value="sessionPricePerKWh"
:valueFmt="fmtAvgPriceShort"
align="end"
/>
<LabelAndValue
class="d-none d-sm-block"
:label="$t('main.loadpoint.avgPrice')"
:value="sessionPricePerKWh"
:valueFmt="fmtAvgPrice"
align="end"
/>
</div>
<LabelAndValue
v-else-if="sessionInfo === SESSION.PRICE"
:label="$t('main.loadpoint.price')"
:value="sessionPrice"
:valueFmt="fmtPrice"
align="end"
/>
<div v-else-if="sessionInfo === SESSION.CO2">
<LabelAndValue
class="d-block d-sm-none"
:label="$t('main.loadpoint.co2')"
:value="sessionCo2PerKWh"
:valueFmt="fmtCo2Short"
align="end"
/>
<LabelAndValue
class="d-none d-sm-block"
:label="$t('main.loadpoint.co2')"
:value="sessionCo2PerKWh"
:valueFmt="fmtCo2Medium"
align="end"
/>
</div>
<LabelAndValue
v-else-if="chargeRemainingDurationInterpolated"
:label="$t('main.loadpoint.remaining')"
:value="fmtDuration(chargeRemainingDurationInterpolated)"
align="end"
/>
<LabelAndValue
v-else
:label="$t('main.loadpoint.duration')"
:value="fmtDuration(chargeDurationInterpolated)"
align="end"
/>
</div>
<LoadpointSessionInfo v-bind="sessionInfoProps" />
</div>
<hr class="divider" />
<Vehicle
@ -167,7 +108,7 @@ import collector from "../mixins/collector";
import LoadpointSettingsButton from "./LoadpointSettingsButton.vue";
import LoadpointSettingsModal from "./LoadpointSettingsModal.vue";
import VehicleIcon from "./VehicleIcon";
import { getSessionInfo, setSessionInfo, SESSION } from "../sessionInfo";
import LoadpointSessionInfo from "./LoadpointSessionInfo.vue";
export default {
name: "Loadpoint",
@ -178,6 +119,7 @@ export default {
LabelAndValue,
LoadpointSettingsButton,
LoadpointSettingsModal,
LoadpointSessionInfo,
VehicleIcon,
},
mixins: [formatter, collector],
@ -265,8 +207,6 @@ export default {
guardRemainingInterpolated: this.guardRemaining,
chargeDurationInterpolated: this.chargeDuration,
chargeRemainingDurationInterpolated: this.chargeRemainingDuration,
sessionInfo: getSessionInfo(this.id),
SESSION,
};
},
computed: {
@ -279,6 +219,9 @@ export default {
phasesProps: function () {
return this.collectProps(Phases);
},
sessionInfoProps: function () {
return this.collectProps(LoadpointSessionInfo);
},
settingsModal: function () {
return this.collectProps(LoadpointSettingsModal);
},
@ -294,17 +237,6 @@ export default {
socBasedCharging: function () {
return (!this.vehicleFeatureOffline && this.vehiclePresent) || this.vehicleSoc > 0;
},
sessionOptions: function () {
const result = [SESSION.TIME, SESSION.SOLAR];
if (this.currency) {
result.push(SESSION.PRICE);
result.push(SESSION.AVG_PRICE);
}
if (this.tariffCo2 > 0 || this.sessionCo2PerKWh > 0) {
result.push(SESSION.CO2);
}
return result;
},
},
watch: {
phaseRemaining() {
@ -391,25 +323,6 @@ export default {
const inKw = value == 0 || value >= 1000;
return this.fmtKWh(value, inKw);
},
fmtPrice(value) {
return `${this.fmtMoney(value, this.currency)} ${this.fmtCurrencySymbol(
this.currency
)}`;
},
fmtAvgPrice(value) {
return this.fmtPricePerKWh(value, this.currency, false);
},
fmtAvgPriceShort(value) {
return this.fmtPricePerKWh(value, this.currency, true);
},
fmtSolar(value) {
return `${this.fmtNumber(value, 1)}%`;
},
nextSessionInfo() {
const index = this.sessionOptions.indexOf(this.sessionInfo);
this.sessionInfo = this.sessionOptions[index + 1] || this.sessionOptions[0];
setSessionInfo(this.id, this.sessionInfo);
},
},
};
</script>
@ -449,8 +362,4 @@ export default {
margin: 0 -1.5rem;
}
}
.sessionInfo {
cursor: pointer;
user-select: none;
}
</style>

View file

@ -0,0 +1,158 @@
<template>
<div class="sessionInfo">
<LabelAndValue class="d-block" align="end">
<template #label>
<CustomSelect
:selected="selectedKey"
:options="selectOptions"
data-testid="sessionInfoSelect"
@change="selectOption($event.target.value)"
>
<span class="text-decoration-underline" data-testid="sessionInfoLabel">
{{ label }}
</span>
</CustomSelect>
</template>
<template #value>
<div data-testid="sessionInfoValue" @click="nextSessionInfo">
<div :class="{ 'd-none d-sm-block': showSm }">{{ value }}</div>
<div v-if="showSm" class="d-block d-sm-none">{{ valueSm }}</div>
</div>
</template>
</LabelAndValue>
</div>
</template>
<script>
import LabelAndValue from "./LabelAndValue.vue";
import formatter from "../mixins/formatter";
import CustomSelect from "./CustomSelect.vue";
import { getSessionInfo, setSessionInfo } from "../sessionInfo";
export default {
name: "LoadpointSessionInfo",
components: {
LabelAndValue,
CustomSelect,
},
mixins: [formatter],
props: {
id: Number,
sessionEnergy: Number,
sessionCo2PerKWh: Number,
sessionPricePerKWh: Number,
sessionPrice: Number,
currency: String,
sessionSolarPercentage: Number,
chargeRemainingDurationInterpolated: Number,
chargeDurationInterpolated: Number,
tariffCo2: Number,
tariffGrid: Number,
},
data() {
return {
selectedKey: getSessionInfo(this.id),
};
},
computed: {
options: function () {
const result = [
{
key: "remaining",
value: this.fmtDuration(this.chargeRemainingDurationInterpolated),
available: this.chargeRemainingDurationInterpolated > 0,
},
{
key: "duration",
value: this.fmtDuration(this.chargeDurationInterpolated),
},
{
key: "solar",
value: this.solarFormatted,
},
{
key: "avgPrice",
value: this.fmtAvgPrice(this.sessionPricePerKWh),
valueSm: this.fmtAvgPriceShort(this.sessionPricePerKWh),
available: this.tariffGrid !== undefined,
},
{
key: "price",
value: this.priceFormatted,
available: this.tariffGrid !== undefined,
},
{
key: "co2",
value: this.fmtCo2Medium(this.sessionCo2PerKWh),
valueSm: this.fmtCo2Short(this.sessionCo2PerKWh),
available: this.tariffCo2 !== undefined,
},
];
// only show options that are available
return result.filter(({ available }) => available === undefined || available);
},
optionKeys() {
return this.options.map((option) => option.key);
},
selectOptions() {
return this.optionKeys.map((key) => ({
name: this.$t(`main.loadpoint.${key}`),
value: key,
}));
},
selectedOption() {
return (
this.options.find((option) => option.key === this.selectedKey) || this.options[0]
);
},
label() {
return this.$t(`main.loadpoint.${this.selectedOption.key}`);
},
value() {
return this.selectedOption.value;
},
valueSm() {
return this.selectedOption.valueSm;
},
showSm() {
return this.valueSm !== undefined;
},
solarFormatted() {
return `${this.fmtNumber(this.sessionSolarPercentage, 1)}%`;
},
priceFormatted() {
return `${this.fmtMoney(this.sessionPrice, this.currency)} ${this.fmtCurrencySymbol(
this.currency
)}`;
},
},
methods: {
fmtAvgPrice(value) {
return this.fmtPricePerKWh(value, this.currency, false);
},
fmtAvgPriceShort(value) {
return this.fmtPricePerKWh(value, this.currency, true);
},
nextSessionInfo() {
const index = this.optionKeys.indexOf(this.selectedKey);
this.selectedKey = this.optionKeys[index + 1] || this.optionKeys[0];
this.presist();
},
selectOption(value) {
this.selectedKey = value;
this.presist();
},
presist() {
setSessionInfo(this.id, this.selectedKey);
},
},
};
</script>
<style scoped>
.sessionInfo * {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
</style>

View file

@ -4,10 +4,10 @@
class="btn btn-link pe-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end"
@click="openModal"
>
<span class="d-inline d-sm-none">{{
<span class="d-inline d-sm-none text-decoration-underline">{{
$t("footer.savings.footerShort", { percent })
}}</span
><span class="d-none d-sm-inline">{{
><span class="d-none d-sm-inline text-decoration-underline">{{
$t("footer.savings.footerLong", { percent })
}}</span>
<shopicon-regular-sun class="ms-2 text-evcc"></shopicon-regular-sun>

View file

@ -7,7 +7,7 @@
class="btn btn-link ps-0 text-decoration-none evcc-default-text text-nowrap d-flex align-items-end"
>
<Logo class="logo me-2" />
v{{ installed }}
<span class="text-decoration-underline">v{{ installed }}</span>
<shopicon-regular-moonstars class="ms-2 text-gray-light"></shopicon-regular-moonstars>
</a>
<button
@ -17,7 +17,7 @@
@click="openModal"
>
<shopicon-regular-gift class="me-2"></shopicon-regular-gift>
v{{ installed }}
<span class="text-decoration-underline">v{{ installed }}</span>
<span class="ms-2 d-none d-sm-block text-gray-medium text-decoration-underline">
{{ $t("footer.version.availableLong") }}
</span>
@ -29,7 +29,7 @@
class="btn btn-link evcc-default-text ps-0 text-decoration-none text-nowrap d-flex align-items-end"
>
<Logo class="logo me-2" />
v{{ installed }}
<span class="text-decoration-underline">v{{ installed }}</span>
</a>
<Teleport to="body">

View file

@ -1,20 +1,7 @@
import settings from "./settings";
const TIME = "time";
const CO2 = "co2";
const PRICE = "price";
const AVG_PRICE = "avgPrice";
const SOLAR = "solar";
export const SESSION = {
TIME,
AVG_PRICE,
PRICE,
SOLAR,
CO2,
};
export function getSessionInfo(index) {
return settings.sessionInfo[index - 1] || TIME;
export function getSessionInfo(index, fallback) {
return settings.sessionInfo[index - 1] || fallback;
}
export function setSessionInfo(index, value) {

View file

@ -1,3 +1,5 @@
interval: 0.1s
site:
title: Hello World
meters:
@ -10,10 +12,18 @@ meters:
source: js
script: |
1000
- name: charger_meter
type: custom
power:
source: js
script: |
500
loadpoints:
- title: Carport
charger: charger
meter: charger_meter
mode: now
chargers:
- name: charger
@ -24,11 +34,12 @@ chargers:
enabled:
source: js
script: |
false
true
status:
source: js
script: |
"B"
"C"
maxcurrent:
source: js
script:
script: |
16

View file

@ -35,3 +35,24 @@ test.describe("main screen", async () => {
await expect(page.getByRole("heading", { name: "Guest vehicle" })).toBeVisible();
});
});
test.describe("session info", async () => {
test("default", async ({ page }) => {
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
});
test("change value", async ({ page }) => {
// by select
await page.getByTestId("sessionInfoSelect").first().selectOption({ label: "Solar" });
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
// by click on value
await page.getByTestId("sessionInfoValue").first().click();
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
});
test("keep selection on reload", async ({ page }) => {
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Duration");
await page.getByTestId("sessionInfoSelect").first().selectOption({ label: "Solar" });
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
await page.reload();
await expect(page.getByTestId("sessionInfoLabel").first()).toContainText("Solar");
});
});