Sessions: add charge duration (#9263)
This commit is contained in:
parent
ee0faa6aff
commit
3202cbb592
17 changed files with 278 additions and 160 deletions
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
33
assets/js/mixins/breakpoint.js
Normal file
33
assets/js/mixins/breakpoint.js
Normal 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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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%");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue