Sessions: add charge duration (#9263)

This commit is contained in:
Michael Geers 2023-08-16 15:30:24 +02:00 • committed by GitHub
parent ee0faa6aff
commit 3202cbb592
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 278 additions and 160 deletions

View file

@ -63,16 +63,15 @@
</td>
</tr>
<tr>
<th>
<th class="align-baseline">
{{ $t("sessions.energy") }}
</th>
<td>
{{
fmtKWh(
session.chargedEnergy * 1e3,
session.chargedEnergy >= 1
)
}}
{{ fmtKWh(chargedEnergy, chargedEnergy >= 1e3) }}
<div v-if="session.chargeDuration">
{{ fmtDurationNs(session.chargeDuration) }}
(~{{ fmtKw(avgPower) }})
</div>
</td>
</tr>
<tr v-if="session.solarPercentage != null">
@ -81,9 +80,7 @@
</th>
<td>
{{ fmtNumber(session.solarPercentage, 1) }}% ({{
fmtKWh(
session.chargedEnergy * 10 * session.solarPercentage
)
fmtKWh(solarEnergy, solarEnergy >= 1e3)
}})
</td>
</tr>
@ -195,6 +192,18 @@ export default {
vehicles: [Object],
},
emits: ["session-changed"],
computed: {
chargedEnergy: function () {
return this.session.chargedEnergy * 1e3;
},
avgPower: function () {
const hours = this.session.chargeDuration / 1e9 / 3600;
return this.chargedEnergy / hours;
},
solarEnergy: function () {
return this.chargedEnergy * (this.session.solarPercentage / 100);
},
},
methods: {
openSessionDetailsModal() {
const modal = Modal.getOrCreateInstance(document.getElementById("sessionDetailsModal"));

View file

@ -2,10 +2,10 @@
<label class="position-relative d-block">
<select :value="selected" class="custom-select" @change="change">
<option
v-for="{ name, value, count } in options"
v-for="{ name, value, count, disabled } in options"
:key="value"
:value="value"
:disabled="count === 0"
:disabled="count === 0 || disabled"
>
{{ text(name, count) }}
</option>

View file

@ -128,19 +128,13 @@
<LabelAndValue
v-else-if="chargeRemainingDurationInterpolated"
:label="$t('main.loadpoint.remaining')"
:value="`
${fmtShortDuration(chargeRemainingDurationInterpolated)}
${fmtShortDurationUnit(chargeRemainingDurationInterpolated, true)}
`"
:value="fmtDuration(chargeRemainingDurationInterpolated)"
align="end"
/>
<LabelAndValue
v-else
:label="$t('main.loadpoint.duration')"
:value="`
${fmtShortDuration(chargeDurationInterpolated)}
${fmtShortDurationUnit(chargeDurationInterpolated)}
`"
:value="fmtDuration(chargeDurationInterpolated)"
align="end"
/>
</div>

View file

@ -42,7 +42,7 @@ export default {
},
computed: {
planDuration() {
return this.fmtShortDuration(this.duration, true);
return this.fmtDuration(this.duration);
},
isCo2() {
return this.smartCostType === CO2_TYPE;

View file

@ -82,7 +82,7 @@ describe("timer", () => {
pvRemainingInterpolated: 90,
},
"pvEnable",
{ remaining: "1:30m" }
{ remaining: "1:30\u202Fm" }
);
});
test("don't show pv enable timer if value is zero", () => {
@ -104,7 +104,7 @@ describe("timer", () => {
pvRemainingInterpolated: 90,
},
"pvDisable",
{ remaining: "1:30m" }
{ remaining: "1:30\u202Fm" }
);
});
test("show phase enable timer if it exists", () => {
@ -116,7 +116,7 @@ describe("timer", () => {
phaseRemainingInterpolated: 90,
},
"scale1p",
{ remaining: "1:30m" }
{ remaining: "1:30\u202Fm" }
);
});
test("show phase disable timer if it exists", () => {
@ -128,7 +128,7 @@ describe("timer", () => {
phaseRemainingInterpolated: 90,
},
"scale3p",
{ remaining: "1:30m" }
{ remaining: "1:30\u202Fm" }
);
});
test("show guard timer if it exists", () => {
@ -139,7 +139,7 @@ describe("timer", () => {
guardRemainingInterpolated: 90,
},
"guard",
{ remaining: "1:30m" }
{ remaining: "1:30\u202Fm" }
);
});
test("don't show guard timer if another timer exists", () => {
@ -153,7 +153,7 @@ describe("timer", () => {
guardRemainingInterpolated: 90,
},
"pvDisable",
{ remaining: "30s" }
{ remaining: "30\u202Fs" }
);
});
test("show guard timer if charging and no other timer exists", () => {
@ -165,7 +165,7 @@ describe("timer", () => {
guardRemainingInterpolated: 90,
},
"guard",
{ remaining: "1:30m" }
{ remaining: "1:30\u202Fm" }
);
});
});

View file

@ -89,7 +89,7 @@ export default {
if (this.pvTimerActive && !this.enabled && this.pvAction === "enable") {
return t("pvEnable", {
remaining: this.fmtShortDuration(this.pvRemainingInterpolated, true),
remaining: this.fmtDuration(this.pvRemainingInterpolated),
});
}
@ -106,19 +106,19 @@ export default {
if (this.pvTimerActive && this.charging && this.pvAction === "disable") {
return t("pvDisable", {
remaining: this.fmtShortDuration(this.pvRemainingInterpolated, true),
remaining: this.fmtDuration(this.pvRemainingInterpolated),
});
}
if (this.phaseTimerActive) {
return t(this.phaseAction, {
remaining: this.fmtShortDuration(this.phaseRemainingInterpolated, true),
remaining: this.fmtDuration(this.phaseRemainingInterpolated),
});
}
if (this.guardTimerActive) {
return t("guard", {
remaining: this.fmtShortDuration(this.guardRemainingInterpolated, true),
remaining: this.fmtDuration(this.guardRemainingInterpolated),
});
}

View file

@ -0,0 +1,33 @@
export default {
data() {
return {
breakpoint: "md",
};
},
methods: {
updateBreakpoint() {
const width = window.innerWidth;
if (width <= 575) {
this.breakpoint = "xs";
} else if (width <= 767) {
this.breakpoint = "sm";
} else if (width <= 991) {
this.breakpoint = "md";
} else if (width <= 1199) {
this.breakpoint = "lg";
} else if (width <= 1399) {
this.breakpoint = "xl";
} else {
this.breakpoint = "xxl";
}
},
},
mounted() {
this.updateBreakpoint();
window.addEventListener("resize", this.updateBreakpoint);
},
beforeDestroy() {
window.removeEventListener("resize", this.updateBreakpoint);
},
};

View file

@ -72,19 +72,13 @@ export default {
fmtUnit: function (val) {
return Math.abs(val) >= this.fmtLimit ? "k" : "";
},
fmtDuration: function (d) {
if (d <= 0 || d == null) {
return "—";
}
var seconds = "0" + (d % 60);
var minutes = "0" + (Math.floor(d / 60) % 60);
var hours = "" + Math.floor(d / 3600);
if (hours.length < 2) {
hours = "0" + hours;
}
return hours + ":" + minutes.substr(-2) + ":" + seconds.substr(-2);
fmtNumberToLocale(val, pad = 0) {
return val.toLocaleString(this.$i18n.locale).padStart(pad, "0");
},
fmtShortDuration: function (duration = 0, withUnit = false) {
fmtDurationNs(duration = 0, withUnit = true, minUnit = "s") {
return this.fmtDuration(duration / 1e9, withUnit, minUnit);
},
fmtDuration: function (duration = 0, withUnit = true, minUnit = "s") {
if (duration <= 0) {
return "—";
}
@ -93,32 +87,22 @@ export default {
var minutes = Math.floor(duration / 60) % 60;
var hours = Math.floor(duration / 3600);
var result = "";
if (hours >= 1) {
result = hours + ":" + `${minutes}`.padStart(2, "0");
} else if (minutes >= 1) {
result = minutes + ":" + `${seconds}`.padStart(2, "0");
let unit = "";
if (hours >= 1 || minUnit === "h") {
result = `${this.fmtNumberToLocale(hours)}:${this.fmtNumberToLocale(minutes, 2)}`;
unit = "h";
} else if (minutes >= 1 || minUnit === "m") {
result = `${this.fmtNumberToLocale(minutes)}:${this.fmtNumberToLocale(seconds, 2)}`;
unit = "m";
} else {
result = `${seconds}`;
result = `${this.fmtNumberToLocale(seconds)}`;
unit = "s";
}
if (withUnit) {
result += this.fmtShortDurationUnit(duration);
result += `\u202F${unit}`;
}
return result;
},
fmtShortDurationUnit: function (duration = 0) {
if (duration <= 0) {
return "";
}
var minutes = Math.floor(duration / 60) % 60;
var hours = Math.floor(duration / 3600);
if (hours >= 1) {
return "h";
}
if (minutes >= 1) {
return "m";
}
return "s";
},
fmtDayString: function (date) {
const YY = `${date.getFullYear()}`;
const MM = `${date.getMonth() + 1}`.padStart(2, "0");

View file

@ -68,3 +68,35 @@ describe("pricePerKWhUnit", () => {
expect(fmt.pricePerKWhUnit("CHF")).eq("rp/kWh");
});
});
describe("fmtDuration", () => {
test("should format zero duration", () => {
expect(fmt.fmtDuration(0)).eq("—");
expect(fmt.fmtDuration(-100)).eq("—");
});
test("should format seconds", () => {
expect(fmt.fmtDuration(1)).eq("1\u202Fs");
expect(fmt.fmtDuration(59)).eq("59\u202Fs");
expect(fmt.fmtDuration(59, false)).eq("59");
expect(fmt.fmtDuration(59, true, "m")).eq("0:59\u202Fm");
expect(fmt.fmtDuration(59, true, "h")).eq("0:00\u202Fh");
});
test("should format minutes", () => {
expect(fmt.fmtDuration(60)).eq("1:00\u202Fm");
expect(fmt.fmtDuration(150)).eq("2:30\u202Fm");
expect(fmt.fmtDuration(150, false)).eq("2:30");
expect(fmt.fmtDuration(150, true, "h")).eq("0:02\u202Fh");
});
test("should format hours", () => {
expect(fmt.fmtDuration(60 * 60)).eq("1:00\u202Fh");
expect(fmt.fmtDuration(60 * 60 * 2.5)).eq("2:30\u202Fh");
expect(fmt.fmtDuration(60 * 60 * 2.5, false)).eq("2:30");
});
test("should format internationalized", () => {
config.global.mocks["$i18n"].locale = "ar-EG";
expect(fmt.fmtDuration(30, false)).eq("٣٠");
expect(fmt.fmtDuration(90, false)).eq("١:٣٠");
expect(fmt.fmtDuration(60 * 100, false)).eq("١:٤٠");
config.global.mocks["$i18n"].locale = "de-DE";
});
});

View file

@ -6,6 +6,7 @@ const SETTINGS_UNIT = "settings_unit";
const SETTINGS_HIDDEN_FEATURES = "settings_hidden_features";
const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
const SESSION_INFO = "session_info";
const SESSION_COLUMNS = "session_columns";
function read(key) {
return window.localStorage[key];
@ -36,7 +37,8 @@ function saveBool(key) {
}
function readArray(key) {
return (read(key) || "").split(",");
const value = read(key);
return value ? value.split(",") : [];
}
function saveArray(key) {
@ -53,6 +55,7 @@ const settings = reactive({
hiddenFeatures: readBool(SETTINGS_HIDDEN_FEATURES),
energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
sessionInfo: readArray(SESSION_INFO),
sessionColumns: readArray(SESSION_COLUMNS),
});
watch(() => settings.locale, save(SETTINGS_LOCALE));
@ -61,5 +64,6 @@ watch(() => settings.unit, save(SETTINGS_UNIT));
watch(() => settings.hiddenFeatures, saveBool(SETTINGS_HIDDEN_FEATURES));
watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
watch(() => settings.sessionInfo, saveArray(SESSION_INFO));
watch(() => settings.sessionColumns, saveArray(SESSION_COLUMNS));
export default settings;

View file

@ -135,35 +135,27 @@
</div>
</th>
<th
v-for="column in columns"
v-for="(column, index) in columnsPerBreakpoint"
:key="column.name"
scope="col"
:data-testid="`sessions-head-${column.name}`"
class="align-top text-end d-none d-md-table-cell"
class="align-top text-end"
>
{{ $t(`sessions.${column.name}`) }}<br />
<div class="text-gray fw-normal">{{ column.unit }}</div>
</th>
<th
scope="col"
class="align-top text-end d-md-none"
data-testid="sessions-head-mobile"
>
<span v-if="columns.length === 1">
{{ $t(`sessions.${mobileColumn.name}`) }}
</span>
<CustomSelect
v-else
:selected="selectedMobileColumn"
:options="mobileColumnOptions"
data-testid="mobile-column"
@change="changeMobileColumn"
v-if="tooMuchColumns"
:selected="column.name"
:options="columnOptions"
data-testid="column"
@change="selectColumnPosition(index, $event.target.value)"
>
<span class="text-decoration-underline">
{{ $t(`sessions.${mobileColumn.name}`) }}
{{ $t(`sessions.${column.name}`) }}
</span>
</CustomSelect>
<div class="text-gray fw-normal">{{ mobileColumn.unit }}</div>
<span v-else>
{{ $t(`sessions.${column.name}`) }}
</span>
<div class="text-gray fw-normal">{{ column.unit }}</div>
</th>
</tr>
</thead>
@ -184,21 +176,14 @@
></th>
<th scope="col" class="d-md-none"></th>
<th
v-for="column in columns"
v-for="column in columnsPerBreakpoint"
:key="column.name"
:data-testid="`sessions-foot-${column.name}`"
scope="col"
class="align-top text-end d-none d-md-table-cell"
class="align-top text-end"
>
{{ column.format(column.total) }}
</th>
<th
scope="col"
data-testid="sessions-foot-mobile"
class="align-top text-end d-md-none"
>
{{ mobileColumn.format(mobileColumn.total) }}
</th>
</tr>
</tfoot>
<tbody>
@ -223,26 +208,15 @@
<div>{{ session.vehicle }}</div>
</td>
<td
v-for="column in columns"
v-for="column in columnsPerBreakpoint"
:key="column.name"
class="text-end d-none d-md-table-cell"
class="text-end"
>
<span v-if="column.value(session) === null" class="text-gray">
-
</span>
<span v-else>{{ column.format(column.value(session)) }}</span>
</td>
<td class="text-end d-md-none">
<span
v-if="mobileColumn.value(session) === null"
class="text-gray"
>
-
</span>
<span v-else>{{
mobileColumn.format(mobileColumn.value(session))
}}</span>
</td>
</tr>
</tbody>
</table>
@ -287,11 +261,22 @@ import api from "../api";
import store from "../store";
import CustomSelect from "../components/CustomSelect.vue";
import ChargingSessionModal from "../components/ChargingSessionModal.vue";
import breakpoint from "../mixins/breakpoint";
import settings from "../settings";
const COLUMNS_PER_BREAKPOINT = {
xs: 1,
sm: 2,
md: 3,
lg: 4,
xl: 5,
xxl: 6,
};
export default {
name: "ChargingSessions",
components: { TopNavigation, ChargingSessionModal, CustomSelect },
mixins: [formatter],
mixins: [formatter, breakpoint],
props: {
notifications: Array,
month: { type: Number, default: () => new Date().getMonth() + 1 },
@ -303,7 +288,7 @@ export default {
return {
sessions: [],
selectedSessionId: undefined,
selectedMobileColumn: undefined,
selectedColumns: settings.sessionColumns,
};
},
computed: {
@ -322,6 +307,9 @@ export default {
filteredSessions() {
return this.currentSessions.filter(this.filterByLoadpoint).filter(this.filterByVehicle);
},
maxColumns() {
return COLUMNS_PER_BREAKPOINT[this.breakpoint] || 1;
},
columns() {
const columns = [
{
@ -359,6 +347,26 @@ export default {
value: (session) => session.co2PerKWh,
format: (value) => this.fmtNumber(value, 0),
},
{
name: "chargeDuration",
unit: "h:mm",
total: this.chargeDuration,
value: (session) => session.chargeDuration,
format: (value) => this.fmtDurationNs(value, false, "h"),
},
{
name: "avgPower",
unit: "kW",
total: this.avgPower,
value: (session) => {
if (session.chargedEnergy && session.chargeDuration) {
return session.chargedEnergy / this.nsToHours(session.chargeDuration);
}
return null;
},
format: (value) =>
value ? this.fmtKw(value * 1e3, true, false, 1) : undefined,
},
];
// only columns with values are shown
return columns.filter((column) => {
@ -366,15 +374,33 @@ export default {
return this.currentSessions.some((s) => column.value(s));
});
},
mobileColumn() {
const column = this.columns.find((column) => column.name === this.selectedMobileColumn);
return column || this.columns[0];
tooMuchColumns() {
return this.columns.length > this.maxColumns;
},
mobileColumnOptions() {
sortedColumns() {
const columns = [...this.columns];
let sorted = [];
for (let name of this.selectedColumns) {
if (!name && columns.length) {
sorted.push(columns.shift());
} else if (columns.some((c) => c.name === name)) {
const column = columns.find((c) => c.name === name);
sorted.push(column);
let index = columns.indexOf(column);
columns.splice(index, 1);
}
}
return sorted.concat(columns);
},
columnsPerBreakpoint() {
return this.sortedColumns.slice(0, this.maxColumns);
},
columnOptions() {
return this.columns.map((column) => {
return {
name: this.$t(`sessions.${column.name}`),
value: column.name,
disabled: this.columnsPerBreakpoint.find((c) => c.name === column.name),
};
});
},
@ -409,9 +435,28 @@ export default {
chargedEnergy() {
return this.filteredSessions.reduce((total, s) => total + s.chargedEnergy, 0);
},
chargeDuration() {
return this.filteredSessions.reduce((total, s) => total + s.chargeDuration, 0);
},
price() {
return this.filteredSessions.reduce((total, s) => total + s.price, 0);
},
avgPower() {
const { energy, hours } = this.filteredSessions
.filter((s) => s.chargedEnergy && s.chargeDuration)
.reduce(
(total, s) => {
total.energy += s.chargedEnergy;
total.hours += this.nsToHours(s.chargeDuration);
return total;
},
{ energy: 0, hours: 0 }
);
if (energy && hours) {
return energy / hours;
}
return null;
},
showVehicles() {
return this.hasMultipleVehicles || this.vehicleFilter;
},
@ -448,7 +493,10 @@ export default {
}),
{ emittedCo2: 0, chargedEnergy: 0 }
);
return total.emittedCo2 / total.chargedEnergy;
if (total.chargedEnergy && total.emittedCo2) {
return total.emittedCo2 / total.chargedEnergy;
}
return null;
},
solarPercentage() {
const total = this.filteredSessions
@ -542,6 +590,9 @@ export default {
this.loadSessions();
},
methods: {
nsToHours(ns) {
return ns / 1e9 / 3600;
},
filterByLoadpoint(session) {
return !this.loadpointFilter || session.loadpoint === this.loadpointFilter;
},
@ -558,8 +609,9 @@ export default {
.filter(this.filterByVehicle)
.filter((s) => !loadpoint || s.loadpoint === loadpoint).length;
},
changeMobileColumn(event) {
this.selectedMobileColumn = event.target.value;
selectColumnPosition(index, value) {
this.selectedColumns[index] = value;
settings.sessionColumns = [...this.selectedColumns];
},
changeLoadpointFilter(event) {
const loadpoint = event.target.value || undefined;

View file

@ -20,20 +20,21 @@ import (
// Session is a single charging session
type Session struct {
ID uint `json:"id" csv:"-" gorm:"primarykey"`
Created time.Time `json:"created"`
Finished time.Time `json:"finished"`
Loadpoint string `json:"loadpoint"`
Identifier string `json:"identifier"`
Vehicle string `json:"vehicle"`
Odometer *float64 `json:"odometer" format:"int"`
MeterStart *float64 `json:"meterStart" csv:"Meter Start (kWh)" gorm:"column:meter_start_kwh"`
MeterStop *float64 `json:"meterStop" csv:"Meter Stop (kWh)" gorm:"column:meter_end_kwh"`
ChargedEnergy float64 `json:"chargedEnergy" csv:"Charged Energy (kWh)" gorm:"column:charged_kwh"`
SolarPercentage *float64 `json:"solarPercentage" csv:"Solar (%)" gorm:"column:solar_percentage"`
Price *float64 `json:"price" csv:"Price" gorm:"column:price"`
PricePerKWh *float64 `json:"pricePerKWh" csv:"Price/kWh" gorm:"column:price_per_kwh"`
Co2PerKWh *float64 `json:"co2PerKWh" csv:"CO2/kWh (gCO2eq)" gorm:"column:co2_per_kwh"`
ID uint `json:"id" csv:"-" gorm:"primarykey"`
Created time.Time `json:"created"`
Finished time.Time `json:"finished"`
Loadpoint string `json:"loadpoint"`
Identifier string `json:"identifier"`
Vehicle string `json:"vehicle"`
Odometer *float64 `json:"odometer" format:"int"`
MeterStart *float64 `json:"meterStart" csv:"Meter Start (kWh)" gorm:"column:meter_start_kwh"`
MeterStop *float64 `json:"meterStop" csv:"Meter Stop (kWh)" gorm:"column:meter_end_kwh"`
ChargedEnergy float64 `json:"chargedEnergy" csv:"Charged Energy (kWh)" gorm:"column:charged_kwh"`
ChargeDuration *time.Duration `json:"chargeDuration" csv:"Charge Duration" gorm:"column:charge_duration"`
SolarPercentage *float64 `json:"solarPercentage" csv:"Solar (%)" gorm:"column:solar_percentage"`
Price *float64 `json:"price" csv:"Price" gorm:"column:price"`
PricePerKWh *float64 `json:"pricePerKWh" csv:"Price/kWh" gorm:"column:price_per_kwh"`
Co2PerKWh *float64 `json:"co2PerKWh" csv:"CO2/kWh (gCO2eq)" gorm:"column:co2_per_kwh"`
}
// Sessions is a list of sessions

View file

@ -73,6 +73,7 @@ func (lp *Loadpoint) stopSession() {
s.PricePerKWh = lp.sessionEnergy.PricePerKWh()
s.Co2PerKWh = lp.sessionEnergy.Co2PerKWh()
s.ChargedEnergy = lp.sessionEnergy.TotalWh() / 1e3
s.ChargeDuration = &lp.chargeDuration
lp.db.Persist(s)
}

View file

@ -145,7 +145,7 @@ selfConsumption = "Eigenverbrauch"
avgPrice = "⌀ Preis"
charged = "Geladen"
co2 = "⌀ CO₂"
duration = "Dauer"
duration = "Ladedauer"
fallbackName = "Ladepunkt"
power = "Leistung"
price = "Σ Preis"
@ -198,7 +198,7 @@ descriptionEnergy = "Bis wann sollen {targetEnergy} ins Fahrzeug geladen sein?"
descriptionSoc = "Wann soll das Fahrzeug auf {targetSoc}% geladen sein?"
inactiveLabel = "Zielzeit"
onlyInPvMode = "Ladeplanung ist nur im PV-Modus aktiv."
planDuration = "Ladezeit"
planDuration = "Ladedauer"
planPeriodLabel = "Zeitraum"
planPeriodValue = "{start} bis {end}"
planUnknown = "noch unbekannt"
@ -214,7 +214,7 @@ tomorrow = "morgen"
update = "Aktualisieren"
[main.targetChargePlan]
chargeDuration = "Ladezeit"
chargeDuration = "Ladedauer"
co2Label = "CO₂-Emission ⌀"
priceLabel = "Energiepreis"
timeRange = "{day} {range} Uhr"
@ -287,7 +287,9 @@ started = "Startzeit"
title = "Ladevorgang"
[sessions]
avgPower = "⌀ Leistung"
avgPrice = "⌀ Preis"
chargeDuration = "Ladedauer"
co2 = "⌀ CO₂"
csvMonth = "Download {month} CSV"
csvTotal = "Gesamte CSV herunterladen"

View file

@ -287,7 +287,9 @@ started = "Started"
title = "Charging Session"
[sessions]
avgPower = "⌀ Power"
avgPrice = "⌀ Price"
chargeDuration = "Duration"
co2 = "⌀ CO₂"
csvMonth = "Download {month} CSV"
csvTotal = "Download total CSV"

View file

@ -33,6 +33,7 @@ test.describe("basics", async () => {
await expect(page.getByTestId("sessions-nodata")).toHaveCount(0);
await expect(page.getByRole("table")).toBeVisible();
await expect(page.getByTestId("sessions-head")).toHaveCount(1);
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(9);
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible("20.0");
@ -46,6 +47,16 @@ test.describe("basics", async () => {
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible("27.5");
await expect(page.getByTestId("sessions-head-chargeDuration")).toContainText("Durationh:mm");
await expect(page.getByTestId("sessions-foot-chargeDuration")).toBeVisible("1:30");
await page
.getByTestId("sessions-head-chargeDuration")
.getByRole("combobox")
.selectOption("⌀ Power");
await expect(page.getByTestId("sessions-head-avgPower")).toContainText("⌀ PowerkW");
await expect(page.getByTestId("sessions-foot-avgPower")).toBeVisible("1:30");
await expect(page.getByTestId("sessions-entry")).toHaveCount(4);
});
});
@ -55,40 +66,32 @@ test.describe("mobile basics", async () => {
await page.setViewportSize(mobile);
await page.goto("/#/sessions?year=2023&month=5");
// hidden columns
await expect(page.getByTestId("sessions-head-energy")).not.toBeVisible();
await expect(page.getByTestId("sessions-foot-energy")).not.toBeVisible();
await expect(page.getByTestId("sessions-head-solar")).not.toBeVisible();
await expect(page.getByTestId("sessions-foot-solar")).not.toBeVisible();
await expect(page.getByTestId("sessions-head-price")).not.toBeVisible();
await expect(page.getByTestId("sessions-foot-price")).not.toBeVisible();
await expect(page.getByTestId("sessions-head-avgPrice")).not.toBeVisible();
await expect(page.getByTestId("sessions-foot-avgPrice")).not.toBeVisible();
await expect(page.getByTestId("sessions-head").locator("th")).toHaveCount(5);
await expect(page.getByTestId("sessions-head-mobile")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-mobile")).toBeVisible("20.0");
await expect(page.getByTestId("sessions-head-energy")).toContainText("ChargedkWh");
await expect(page.getByTestId("sessions-foot-energy")).toBeVisible("20.0");
await page.getByTestId("mobile-column").selectOption("Solar");
await expect(page.getByTestId("sessions-head-mobile")).toContainText("Solar%");
await expect(page.getByTestId("sessions-foot-mobile")).toBeVisible("67.3");
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
await expect(page.getByTestId("sessions-foot-solar")).toBeVisible("67.3");
await page.getByTestId("mobile-column").selectOption("Σ Price");
await expect(page.getByTestId("sessions-head-mobile")).toContainText("Σ Price€");
await expect(page.getByTestId("sessions-foot-mobile")).toBeVisible("5.50");
await page.getByTestId("sessions-head-solar").getByRole("combobox").selectOption("Σ Price");
await expect(page.getByTestId("sessions-head-price")).toContainText("Σ Price€");
await expect(page.getByTestId("sessions-foot-price")).toBeVisible("5.50");
await page.getByTestId("mobile-column").selectOption("⌀ Price");
await expect(page.getByTestId("sessions-head-mobile")).toContainText("⌀ Pricect/kWh");
await expect(page.getByTestId("sessions-foot-mobile")).toBeVisible("27.5");
await page.getByTestId("sessions-head-price").getByRole("combobox").selectOption("⌀ Price");
await expect(page.getByTestId("sessions-head-avgPrice")).toContainText("⌀ Pricect/kWh");
await expect(page.getByTestId("sessions-foot-avgPrice")).toBeVisible("27.5");
});
test("keep selection when paging", async ({ page }) => {
await page.setViewportSize(mobile);
await page.goto("/#/sessions?year=2023&month=5");
await page.getByTestId("mobile-column").selectOption("Solar");
await page.getByTestId("sessions-head-energy").getByRole("combobox").selectOption("Solar");
await page.getByRole("link", { name: "Apr" }).click();
await page.getByRole("link", { name: "May" }).click();
await expect(page.getByTestId("sessions-head-mobile")).toContainText("Solar%");
await expect(page.getByTestId("sessions-head-solar")).toContainText("Solar%");
});
});

View file

@ -14,11 +14,12 @@ CREATE TABLE `sessions` (
`price` real,
`price_per_kwh` real,
`co2_per_kwh` real,
`charge_duration` integer,
PRIMARY KEY (`id`)
);
INSERT INTO `sessions` VALUES (1,'2023-03-01 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,NULL,100,2,0.2,300);
INSERT INTO `sessions` VALUES (2,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,NULL,100,2,0.2,NULL);
INSERT INTO `sessions` VALUES (3,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,2.5,NULL,88.21,0.75,0.3,NULL);
INSERT INTO `sessions` VALUES (4,'2023-05-03 16:00:00.0+02:00','2023-05-03 20:00:00.0+02:00','Carport',NULL,'weißes Model 3',NULL,NULL,2.5,NULL,50,0.25,0.1,NULL);
INSERT INTO `sessions` VALUES (5,'2023-05-04 22:00:00.0+02:00','2023-05-05 06:00:00.0+02:00','Garage',NULL,'weißes Model 3',NULL,NULL,5,NULL,0,2.5,0.5,NULL);
INSERT INTO `sessions` VALUES (1,'2023-03-01 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,NULL,100,2,0.2,300,3600000000000);
INSERT INTO `sessions` VALUES (2,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,10,NULL,100,2,0.2,NULL,1800000000000);
INSERT INTO `sessions` VALUES (3,'2023-05-02 08:00:00.0+02:00','2023-05-02 12:00:00.0+02:00','Carport',NULL,'blauer e-Golf',NULL,NULL,2.5,NULL,88.21,0.75,0.3,NULL,NULL);
INSERT INTO `sessions` VALUES (4,'2023-05-03 16:00:00.0+02:00','2023-05-03 20:00:00.0+02:00','Carport',NULL,'weißes Model 3',NULL,NULL,2.5,NULL,50,0.25,0.1,NULL,NULL);
INSERT INTO `sessions` VALUES (5,'2023-05-04 22:00:00.0+02:00','2023-05-05 06:00:00.0+02:00','Garage',NULL,'weißes Model 3',NULL,NULL,5,NULL,0,2.5,0.5,null,3600000000000);