chore: Typescript: Rewrite Vehicles (#21636)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
e3fd0817f5
commit
040e05b1eb
17 changed files with 740 additions and 635 deletions
2
.github/workflows/default.yml
vendored
2
.github/workflows/default.yml
vendored
|
|
@ -202,6 +202,8 @@ jobs:
|
|||
|
||||
- name: Run tests
|
||||
run: npx playwright test
|
||||
env:
|
||||
TZ: Europe/Berlin
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
data-testid="session-details"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div v-if="session" class="modal-content">
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
<div class="modal-body">
|
||||
<table class="table align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<tr data-testid="session-details-loadpoint">
|
||||
<th>
|
||||
{{ $t("sessions.loadpoint") }}
|
||||
</th>
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
</CustomSelect>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="session-details-vehicle">
|
||||
<th>
|
||||
{{ $t("sessions.vehicle") }}
|
||||
</th>
|
||||
|
|
@ -51,7 +52,7 @@
|
|||
<VehicleOptions
|
||||
:id="session.vehicle"
|
||||
class="options"
|
||||
:vehicles="vehicleOptions"
|
||||
:vehicleOptions="vehicleOptions"
|
||||
connected
|
||||
:selected="session.vehicle"
|
||||
@change-vehicle="changeVehicle"
|
||||
|
|
@ -67,17 +68,21 @@
|
|||
</VehicleOptions>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="session-details-date">
|
||||
<th class="align-baseline">
|
||||
{{ $t("session.date") }}
|
||||
</th>
|
||||
<td>
|
||||
{{ fmtFullDateTime(session.created, false) }}
|
||||
<template v-if="session.created">
|
||||
{{ fmtFullDateTime(new Date(session.created), false) }}
|
||||
</template>
|
||||
<br />
|
||||
{{ fmtFullDateTime(session.finished, false) }}
|
||||
<template v-if="session.finished">
|
||||
{{ fmtFullDateTime(new Date(session.finished), false) }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="session-details-energy">
|
||||
<th class="align-baseline">
|
||||
{{ $t("sessions.energy") }}
|
||||
</th>
|
||||
|
|
@ -96,7 +101,10 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="session.solarPercentage != null">
|
||||
<tr
|
||||
v-if="session.solarPercentage != null"
|
||||
data-testid="session-details-solar"
|
||||
>
|
||||
<th class="align-baseline">
|
||||
{{ $t("sessions.solar") }}
|
||||
</th>
|
||||
|
|
@ -105,7 +113,10 @@
|
|||
({{ fmtWh(solarEnergy, POWER_UNIT.AUTO) }})
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="session.price != null">
|
||||
<tr
|
||||
v-if="session.price != null"
|
||||
data-testid="session-details-price"
|
||||
>
|
||||
<th class="align-baseline">
|
||||
{{ $t("session.price") }}
|
||||
</th>
|
||||
|
|
@ -115,7 +126,10 @@
|
|||
{{ fmtPricePerKWh(session.pricePerKWh || 0, currency) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="session.co2PerKWh != null">
|
||||
<tr
|
||||
v-if="session.co2PerKWh != null"
|
||||
data-testid="session-details-co2"
|
||||
>
|
||||
<th>
|
||||
{{ $t("session.co2") }}
|
||||
</th>
|
||||
|
|
@ -123,7 +137,7 @@
|
|||
{{ fmtCo2Medium(session.co2PerKWh) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="session.odometer">
|
||||
<tr v-if="session.odometer" data-testid="session-details-odometer">
|
||||
<th>
|
||||
{{ $t("session.odometer") }}
|
||||
</th>
|
||||
|
|
@ -131,7 +145,7 @@
|
|||
{{ formatKm(session.odometer) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="session.meterStart">
|
||||
<tr v-if="session.meterStart" data-testid="session-details-meter">
|
||||
<th class="align-baseline">
|
||||
{{ $t("session.meter") }}
|
||||
</th>
|
||||
|
|
@ -148,6 +162,7 @@
|
|||
type="button"
|
||||
class="btn btn-link text-danger"
|
||||
data-bs-dismiss="modal"
|
||||
data-testid="session-details-delete"
|
||||
@click="openRemoveConfirmationModal"
|
||||
>
|
||||
{{ $t("session.delete") }}
|
||||
|
|
@ -228,10 +243,10 @@ export default defineComponent({
|
|||
solarEnergy() {
|
||||
return this.chargedEnergy * (this.session.solarPercentage / 100);
|
||||
},
|
||||
vehicleOptions() {
|
||||
vehicleOptions(): SelectOption<string>[] {
|
||||
return this.vehicles.map((v) => ({
|
||||
name: v.title,
|
||||
title: v.title,
|
||||
value: v.title,
|
||||
}));
|
||||
},
|
||||
loadpointOptions(): SelectOption<string>[] {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export interface Session {
|
||||
id: number;
|
||||
created: Date;
|
||||
finished: Date;
|
||||
created: string;
|
||||
finished: string;
|
||||
loadpoint: string;
|
||||
identifier: string;
|
||||
vehicle: string;
|
||||
|
|
|
|||
|
|
@ -33,13 +33,14 @@
|
|||
</LabelAndValue>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import { estimatedSoc, energyOptions, optionStep, fmtEnergy } from "@/utils/energyOptions";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "LimitEnergySelect",
|
||||
components: { LabelAndValue, AnimatedNumber },
|
||||
mixins: [formatter],
|
||||
|
|
@ -69,17 +70,20 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
change(e) {
|
||||
return this.$emit("limit-energy-updated", parseFloat(e.target.value));
|
||||
change(e: Event) {
|
||||
return this.$emit(
|
||||
"limit-energy-updated",
|
||||
parseFloat((e.target as HTMLSelectElement).value)
|
||||
);
|
||||
},
|
||||
fmtEnergy(value) {
|
||||
fmtEnergy(value: number) {
|
||||
return fmtEnergy(value, this.step, this.fmtWh, this.$t("main.targetEnergy.noLimit"));
|
||||
},
|
||||
fmtSoc(value) {
|
||||
fmtSoc(value: number) {
|
||||
return `+${this.fmtPercentage(value)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -19,18 +19,19 @@
|
|||
</LabelAndValue>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
|
||||
import { distanceUnit } from "@/units";
|
||||
import formatter from "@/mixins/formatter";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "LimitSocSelect",
|
||||
components: { LabelAndValue, AnimatedNumber },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
limitSoc: Number,
|
||||
limitSoc: { type: Number, default: 0 },
|
||||
rangePerSoc: Number,
|
||||
heating: Boolean,
|
||||
},
|
||||
|
|
@ -57,23 +58,26 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
change(e) {
|
||||
return this.$emit("limit-soc-updated", parseInt(e.target.value, 10));
|
||||
change(e: Event) {
|
||||
return this.$emit(
|
||||
"limit-soc-updated",
|
||||
parseInt((e.target as HTMLSelectElement).value, 10)
|
||||
);
|
||||
},
|
||||
estimatedRange(soc) {
|
||||
estimatedRange(soc: number) {
|
||||
if (this.rangePerSoc) {
|
||||
return Math.round(soc * this.rangePerSoc);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
formatSoc(value) {
|
||||
formatSoc(value: number) {
|
||||
return this.heating ? this.fmtTemperature(value) : this.fmtPercentage(value);
|
||||
},
|
||||
formatKm(value) {
|
||||
formatKm(value: number) {
|
||||
return `${this.fmtNumber(value, 0)} ${distanceUnit()}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@
|
|||
>
|
||||
<select :id="dropdownId" :value="selected" class="custom-select" @change="change">
|
||||
<option
|
||||
v-for="{ name, title } in vehicles"
|
||||
v-for="{ name, value } in vehicleOptions"
|
||||
:key="name"
|
||||
:value="name"
|
||||
:selected="name === selected"
|
||||
>
|
||||
{{ title }}
|
||||
{{ value }}
|
||||
</option>
|
||||
<hr />
|
||||
<option value="" :selected="!selected">
|
||||
|
|
@ -23,13 +23,16 @@
|
|||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
<script lang="ts">
|
||||
import type { SelectOption } from "@/types/evcc";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "VehicleOptions",
|
||||
props: {
|
||||
connected: Boolean,
|
||||
id: [String, Number],
|
||||
vehicles: Array,
|
||||
vehicleOptions: Array as PropType<SelectOption<string>[]>,
|
||||
selected: String,
|
||||
},
|
||||
emits: ["change-vehicle", "remove-vehicle"],
|
||||
|
|
@ -39,8 +42,8 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
change(event) {
|
||||
const name = event.target.value;
|
||||
change(event: Event) {
|
||||
const name = (event.target as HTMLSelectElement).value;
|
||||
if (name) {
|
||||
this.$emit("change-vehicle", name);
|
||||
} else {
|
||||
|
|
@ -48,7 +51,7 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.custom-select {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
:style="{ width: `${vehicleSocDisplayWidth}%`, ...transition }"
|
||||
></div>
|
||||
<div
|
||||
v-if="remainingSocWidth > 0 && enabled && connected"
|
||||
v-if="remainingSocWidth !== null && remainingSocWidth > 0 && enabled && connected"
|
||||
class="progress-bar bg-muted"
|
||||
role="progressbar"
|
||||
:class="progressColor"
|
||||
|
|
@ -34,7 +34,8 @@
|
|||
data-bs-toggle="tooltip"
|
||||
:class="{
|
||||
'energy-limit-marker--active': energyLimitMarkerActive,
|
||||
'energy-limit-marker--visible': energyLimitMarkerPosition < 100,
|
||||
'energy-limit-marker--visible':
|
||||
energyLimitMarkerPosition !== null && energyLimitMarkerPosition < 100,
|
||||
}"
|
||||
:style="{ left: `${energyLimitMarkerPosition}%` }"
|
||||
/>
|
||||
|
|
@ -69,36 +70,37 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Tooltip from "bootstrap/js/dist/tooltip";
|
||||
import "@h2d2/shopicons/es/regular/clock";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "VehicleSoc",
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
connected: Boolean,
|
||||
vehicleSoc: Number,
|
||||
vehicleLimitSoc: Number,
|
||||
vehicleSoc: { type: Number, default: 0 },
|
||||
vehicleLimitSoc: { type: Number, default: 0 },
|
||||
enabled: Boolean,
|
||||
charging: Boolean,
|
||||
heating: Boolean,
|
||||
minSoc: Number,
|
||||
effectivePlanSoc: Number,
|
||||
minSoc: { type: Number, default: 0 },
|
||||
effectivePlanSoc: { type: Number, default: 0 },
|
||||
effectiveLimitSoc: Number,
|
||||
limitEnergy: Number,
|
||||
planEnergy: Number,
|
||||
chargedEnergy: Number,
|
||||
limitEnergy: { type: Number, default: 0 },
|
||||
planEnergy: { type: Number, default: 0 },
|
||||
chargedEnergy: { type: Number, default: 0 },
|
||||
socBasedCharging: Boolean,
|
||||
socBasedPlanning: Boolean,
|
||||
},
|
||||
emits: ["limit-soc-drag", "limit-soc-updated", "plan-clicked"],
|
||||
data() {
|
||||
return {
|
||||
selectedLimitSoc: null,
|
||||
selectedLimitSoc: undefined as number | undefined,
|
||||
interactionStartScreenY: null,
|
||||
tooltip: null,
|
||||
tooltip: null as Tooltip | null,
|
||||
dragging: false,
|
||||
};
|
||||
},
|
||||
|
|
@ -131,7 +133,7 @@ export default {
|
|||
vehicleLimitSocActive() {
|
||||
return this.vehicleLimitSoc > 0 && this.vehicleLimitSoc > this.vehicleSoc;
|
||||
},
|
||||
planMarkerPosition() {
|
||||
planMarkerPosition(): number {
|
||||
if (this.socBasedPlanning) {
|
||||
return this.effectivePlanSoc;
|
||||
}
|
||||
|
|
@ -217,24 +219,24 @@ export default {
|
|||
this.updateTooltip();
|
||||
},
|
||||
methods: {
|
||||
changeLimitSocStart(e) {
|
||||
changeLimitSocStart(e: Event) {
|
||||
this.dragging = true;
|
||||
e.stopPropagation();
|
||||
},
|
||||
changeLimitSocEnd(e) {
|
||||
changeLimitSocEnd(e: Event) {
|
||||
this.dragging = false;
|
||||
const value = parseInt(e.target.value, 10);
|
||||
const value = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
// value changed
|
||||
if (value !== this.effectiveLimitSoc) {
|
||||
this.$emit("limit-soc-updated", value);
|
||||
}
|
||||
},
|
||||
movedLimitSoc(e) {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
movedLimitSoc(e: Event) {
|
||||
const value = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
e.stopPropagation();
|
||||
const minLimit = 20;
|
||||
if (value < minLimit) {
|
||||
e.target.value = minLimit;
|
||||
(e.target as HTMLInputElement).value = minLimit.toString();
|
||||
this.selectedLimitSoc = value;
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
|
@ -246,7 +248,7 @@ export default {
|
|||
},
|
||||
updateTooltip() {
|
||||
if (!this.tooltip) {
|
||||
this.tooltip = new Tooltip(this.$refs.vehicleLimitSoc);
|
||||
this.tooltip = new Tooltip(this.$refs["vehicleLimitSoc"] as HTMLElement);
|
||||
}
|
||||
const value = this.heating
|
||||
? this.fmtTemperature(this.vehicleLimitSoc)
|
||||
|
|
@ -256,7 +258,7 @@ export default {
|
|||
this.tooltip.setContent({ ".tooltip-inner": content });
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.vehicle-soc {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import Status from "./Status.vue";
|
||||
import { CURRENCY } from "@/types/evcc";
|
||||
|
||||
function getFutureTime(hours, minutes) {
|
||||
function getFutureTime(hours: number, minutes: number) {
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + hours);
|
||||
now.setMinutes(now.getMinutes() + minutes);
|
||||
|
|
@ -135,7 +136,7 @@ const planProjectedEnd = getFutureTime(5, 43);
|
|||
connected
|
||||
enabled
|
||||
charging
|
||||
currency="CHF"
|
||||
:currency="CURRENCY.EUR"
|
||||
:tariffGrid="0.32"
|
||||
:smartCostLimit="0.12"
|
||||
smartCostType="price"
|
||||
|
|
@ -143,7 +144,7 @@ const planProjectedEnd = getFutureTime(5, 43);
|
|||
</Variant>
|
||||
<Variant title="smart cost: cheap but not connected">
|
||||
<Status
|
||||
currency="EUR"
|
||||
:currency="CURRENCY.EUR"
|
||||
:tariffGrid="0.091"
|
||||
:smartCostLimit="0.12"
|
||||
smartCostType="price"
|
||||
|
|
@ -154,7 +155,7 @@ const planProjectedEnd = getFutureTime(5, 43);
|
|||
connected
|
||||
enabled
|
||||
charging
|
||||
currency="CHF"
|
||||
:currency="CURRENCY.EUR"
|
||||
:smartCostLimit="0.12"
|
||||
smartCostType="price"
|
||||
:smartCostNextStart="planProjectedStart"
|
||||
|
|
@ -165,7 +166,7 @@ const planProjectedEnd = getFutureTime(5, 43);
|
|||
connected
|
||||
enabled
|
||||
charging
|
||||
currency="CHF"
|
||||
:currency="CURRENCY.EUR"
|
||||
:tariffGrid="0.11"
|
||||
:smartCostLimit="0.12"
|
||||
smartCostType="price"
|
||||
|
|
@ -177,7 +178,7 @@ const planProjectedEnd = getFutureTime(5, 43);
|
|||
connected
|
||||
enabled
|
||||
charging
|
||||
currency="EUR"
|
||||
:currency="CURRENCY.EUR"
|
||||
:smartCostLimit="0.15"
|
||||
smartCostType="price"
|
||||
:minSoc="20"
|
||||
|
|
|
|||
|
|
@ -1,294 +0,0 @@
|
|||
import { mount, config } from "@vue/test-utils";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import Status from "./Status.vue";
|
||||
|
||||
const serializeData = (data) => (data ? `:${JSON.stringify(data)}` : "");
|
||||
config.global.mocks["$t"] = (key, data) => `${key}${serializeData(data)}`;
|
||||
config.global.mocks["$i18n"] = { locale: "de-DE" };
|
||||
|
||||
const allEntries = {
|
||||
pvtimer: false,
|
||||
phasetimer: false,
|
||||
solar: false,
|
||||
climater: false,
|
||||
minsoc: false,
|
||||
limit: false,
|
||||
smartcost: false,
|
||||
planactive: false,
|
||||
planstart: false,
|
||||
};
|
||||
|
||||
const expectEntries = (props, entries) => {
|
||||
const expectedEntries = { ...allEntries, ...entries };
|
||||
|
||||
const wrapper = mount(Status, { props });
|
||||
|
||||
Object.entries(expectedEntries).forEach(([key, value]) => {
|
||||
const selector = `[data-testid=vehicle-status-${key}]`;
|
||||
if (typeof value === "boolean") {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(value);
|
||||
} else {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(true);
|
||||
expect(wrapper.find(selector).text(), selector).eq(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe("basics", () => {
|
||||
test("no vehicle is connected", () => {
|
||||
expectEntries({ connected: false }, { charger: "main.vehicleStatus.disconnected" });
|
||||
});
|
||||
test("vehicle is connected", () => {
|
||||
expectEntries({ connected: true }, { charger: "main.vehicleStatus.connected" });
|
||||
});
|
||||
test("show waiting for vehicle if charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
});
|
||||
test("vehicle is charging", () => {
|
||||
expectEntries({ connected: true, charging: true }, { charger: "main.vehicleStatus.charging" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("min charge", () => {
|
||||
test("active when vehicle soc is below", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: "20 %" }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is above", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 21 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is equal", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 20 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when limit is 0", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 0, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plan", () => {
|
||||
const effectivePlanTime = "2020-03-16T06:00:00Z";
|
||||
const planProjectedStart = "2020-03-16T02:00:00Z";
|
||||
const planProjectedEnd = "2020-03-16T05:00:00Z";
|
||||
test("charging if target time is set, status is charging but planned slot is not active", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
});
|
||||
test("active if target time is set, status is charging and planned slot is active", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("waiting for vehicle if a target time is set, the charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("show projected start if not enabled yet", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true },
|
||||
{ charger: "main.vehicleStatus.connected", planstart: "Mo 03:00" }
|
||||
);
|
||||
});
|
||||
test("dont show plan status if plan is disabled (e.g. off, fast mode)", () => {
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: false,
|
||||
enabled: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true, chargingPlanDisabled: true },
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("climating", () => {
|
||||
test("show climating status", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: true },
|
||||
{ charger: "main.vehicleStatus.charging", climater: true }
|
||||
);
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", climater: true }
|
||||
);
|
||||
});
|
||||
test("only show climating if enabled", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: false, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.connected", climater: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timer", () => {
|
||||
test("show pv enable timer if not enabled yet and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("don't show pv enable timer if value is zero", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 0,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
test("show pv disable timer if charging and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "disable",
|
||||
connected: true,
|
||||
charging: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase enable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale1p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase disable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale3p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("vehicle target soc", () => {
|
||||
test("show target reached if charger enabled but soc has reached vehicle limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 70,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
test("show reached message even if vehicle is slightly below its limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 69,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("smart grid charging", () => {
|
||||
test("show clean energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 600,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show clean energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 400,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "400 g ≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.3,
|
||||
smartCostLimit: 0.29,
|
||||
currency: "CHF",
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 29,0 rp" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.28,
|
||||
smartCostLimit: 0.29,
|
||||
currency: "CHF",
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "28,0 rp ≤ 29,0 rp" }
|
||||
);
|
||||
});
|
||||
});
|
||||
298
assets/js/components/Vehicles/Status.test.ts
Normal file
298
assets/js/components/Vehicles/Status.test.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
import { mount, config } from "@vue/test-utils";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import Status from "./Status.vue";
|
||||
import { CURRENCY } from "@/types/evcc";
|
||||
|
||||
const serializeData = (data: Object) => (data ? `:${JSON.stringify(data)}` : "");
|
||||
config.global.mocks["$t"] = (key: string, data: Object) => `${key}${serializeData(data)}`;
|
||||
config.global.mocks["$i18n"] = { locale: "de-DE" };
|
||||
|
||||
const allEntries = {
|
||||
pvtimer: false,
|
||||
phasetimer: false,
|
||||
solar: false,
|
||||
climater: false,
|
||||
minsoc: false,
|
||||
limit: false,
|
||||
smartcost: false,
|
||||
planactive: false,
|
||||
planstart: false,
|
||||
};
|
||||
|
||||
const expectEntries = (props: InstanceType<typeof Status>["$props"], entries: Object) => {
|
||||
const expectedEntries = { ...allEntries, ...entries };
|
||||
|
||||
const wrapper = mount(Status, { props });
|
||||
|
||||
Object.entries(expectedEntries).forEach(([key, value]) => {
|
||||
const selector = `[data-testid=vehicle-status-${key}]`;
|
||||
if (typeof value === "boolean") {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(value);
|
||||
} else {
|
||||
expect(wrapper.find(selector).exists(), selector).eq(true);
|
||||
expect(wrapper.find(selector).text(), selector).eq(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe("basics", () => {
|
||||
test("no vehicle is connected", () => {
|
||||
expectEntries({ connected: false }, { charger: "main.vehicleStatus.disconnected" });
|
||||
});
|
||||
test("vehicle is connected", () => {
|
||||
expectEntries({ connected: true }, { charger: "main.vehicleStatus.connected" });
|
||||
});
|
||||
test("show waiting for vehicle if charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
});
|
||||
test("vehicle is charging", () => {
|
||||
expectEntries(
|
||||
{ connected: true, charging: true },
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("min charge", () => {
|
||||
test("active when vehicle soc is below", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: "20 %" }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is above", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 21 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when vehicle soc is equal", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 20, vehicleSoc: 20 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
test("not active when limit is 0", () => {
|
||||
expectEntries(
|
||||
{ connected: true, minSoc: 0, vehicleSoc: 10 },
|
||||
{ charger: "main.vehicleStatus.connected", minsoc: false }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plan", () => {
|
||||
const effectivePlanTime = "2020-03-16T06:00:00Z";
|
||||
const planProjectedStart = "2020-03-16T02:00:00Z";
|
||||
const planProjectedEnd = "2020-03-16T05:00:00Z";
|
||||
test("charging if target time is set, status is charging but planned slot is not active", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
});
|
||||
test("active if target time is set, status is charging and planned slot is active", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, charging: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.charging", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("waiting for vehicle if a target time is set, the charger is enabled but not charging", () => {
|
||||
expectEntries(
|
||||
{ planProjectedEnd, planActive: true, enabled: true, connected: true },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", planactive: "Mo 06:00" }
|
||||
);
|
||||
});
|
||||
test("show projected start if not enabled yet", () => {
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true },
|
||||
{ charger: "main.vehicleStatus.connected", planstart: "Mo 03:00" }
|
||||
);
|
||||
});
|
||||
test("dont show plan status if plan is disabled (e.g. off, fast mode)", () => {
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging" }
|
||||
);
|
||||
expectEntries(
|
||||
{
|
||||
effectivePlanTime,
|
||||
planActive: true,
|
||||
charging: false,
|
||||
enabled: true,
|
||||
connected: true,
|
||||
chargingPlanDisabled: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.waitForVehicle" }
|
||||
);
|
||||
expectEntries(
|
||||
{ effectivePlanTime, planProjectedStart, connected: true, chargingPlanDisabled: true },
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("climating", () => {
|
||||
test("show climating status", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: true },
|
||||
{ charger: "main.vehicleStatus.charging", climater: true }
|
||||
);
|
||||
expectEntries(
|
||||
{ connected: true, enabled: true, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.waitForVehicle", climater: true }
|
||||
);
|
||||
});
|
||||
test("only show climating if enabled", () => {
|
||||
expectEntries(
|
||||
{ connected: true, enabled: false, vehicleClimaterActive: true, charging: false },
|
||||
{ charger: "main.vehicleStatus.connected", climater: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timer", () => {
|
||||
test("show pv enable timer if not enabled yet and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("don't show pv enable timer if value is zero", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "enable",
|
||||
connected: true,
|
||||
pvRemainingInterpolated: 0,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.connected" }
|
||||
);
|
||||
});
|
||||
test("show pv disable timer if charging and timer exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
pvAction: "disable",
|
||||
connected: true,
|
||||
charging: true,
|
||||
pvRemainingInterpolated: 90,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", pvtimer: "1:30\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase enable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale1p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
test("show phase disable timer if it exists", () => {
|
||||
expectEntries(
|
||||
{
|
||||
phaseAction: "scale3p",
|
||||
connected: true,
|
||||
charging: true,
|
||||
phaseRemainingInterpolated: 91,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", phasetimer: "1:31\u202Fm" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("vehicle target soc", () => {
|
||||
test("show target reached if charger enabled but soc has reached vehicle limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 70,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
test("show reached message even if vehicle is slightly below its limit", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 70,
|
||||
vehicleSoc: 69,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.finished", limit: "70 %" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("smart grid charging", () => {
|
||||
test("show clean energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 600,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show clean energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffCo2: 400,
|
||||
smartCostLimit: 500,
|
||||
smartCostType: "co2",
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "400 g ≤ 500 g" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.3,
|
||||
smartCostLimit: 0.29,
|
||||
currency: CURRENCY.EUR,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "≤ 29,0 ct" }
|
||||
);
|
||||
});
|
||||
test("show cheap energy message if active", () => {
|
||||
expectEntries(
|
||||
{
|
||||
connected: true,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
tariffGrid: 0.28,
|
||||
smartCostLimit: 0.29,
|
||||
currency: CURRENCY.EUR,
|
||||
smartCostActive: true,
|
||||
},
|
||||
{ charger: "main.vehicleStatus.charging", smartcost: "28,0 ct ≤ 29,0 ct" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
:class="vehicleLimitClass"
|
||||
data-bs-toggle="tooltip"
|
||||
data-testid="vehicle-status-limit"
|
||||
:role="vehicleLimitWarning ? 'button' : null"
|
||||
:role="vehicleLimitWarning ? 'button' : undefined"
|
||||
@click="vehicleLimitClicked"
|
||||
>
|
||||
<component :is="vehicleLimitIconComponent" />
|
||||
|
|
@ -158,7 +158,7 @@
|
|||
>
|
||||
<PlanEndIcon />
|
||||
<span>
|
||||
{{ fmtAbsoluteDate(new Date(planProjectedEnd)) }}
|
||||
{{ planProjectedEnd ? fmtAbsoluteDate(new Date(planProjectedEnd)) : "" }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -171,13 +171,13 @@
|
|||
@click="planStartClicked"
|
||||
>
|
||||
<PlanStartIcon />
|
||||
{{ fmtAbsoluteDate(new Date(planProjectedStart)) }}
|
||||
{{ planProjectedStart ? fmtAbsoluteDate(new Date(planProjectedStart)) : "" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/sun";
|
||||
import "@h2d2/shopicons/es/regular/eco1";
|
||||
import "@h2d2/shopicons/es/regular/angledoublerightsmall";
|
||||
|
|
@ -201,10 +201,12 @@ import VehicleLimitWarningIcon from "../MaterialIcon/VehicleLimitWarning.vue";
|
|||
import VehicleMinSocIcon from "../MaterialIcon/VehicleMinSoc.vue";
|
||||
import WelcomeIcon from "../MaterialIcon/Welcome.vue";
|
||||
import BatteryBoostIcon from "../MaterialIcon/BatteryBoost.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { CURRENCY, Timeout } from "@/types/evcc";
|
||||
const REASON_AUTH = "waitingforauthorization";
|
||||
const REASON_DISCONNECT = "disconnectrequired";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "VehicleStatus",
|
||||
components: {
|
||||
ClimaterIcon,
|
||||
|
|
@ -223,71 +225,74 @@ export default {
|
|||
},
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
vehicleSoc: Number,
|
||||
vehicleSoc: { type: Number, default: 0 },
|
||||
batteryBoostActive: Boolean,
|
||||
charging: Boolean,
|
||||
chargingPlanDisabled: Boolean,
|
||||
chargerStatusReason: String,
|
||||
connected: Boolean,
|
||||
currency: String,
|
||||
currency: String as PropType<CURRENCY>,
|
||||
effectiveLimitSoc: Number,
|
||||
effectivePlanSoc: Number,
|
||||
effectivePlanSoc: { type: Number, default: 0 },
|
||||
effectivePlanTime: String,
|
||||
enabled: Boolean,
|
||||
heating: Boolean,
|
||||
minSoc: Number,
|
||||
phaseAction: String,
|
||||
minSoc: { type: Number, default: 0 },
|
||||
phaseAction: { type: String, default: "" },
|
||||
phaseRemainingInterpolated: Number,
|
||||
planActive: Boolean,
|
||||
planOverrun: Number,
|
||||
planProjectedEnd: String,
|
||||
planProjectedStart: String,
|
||||
planTimeUnreachable: Boolean,
|
||||
pvAction: String,
|
||||
pvAction: { type: String, default: "" },
|
||||
pvRemainingInterpolated: Number,
|
||||
smartCostActive: Boolean,
|
||||
smartCostDisabled: Boolean,
|
||||
smartCostLimit: { type: Number, default: null },
|
||||
smartCostNextStart: String,
|
||||
smartCostType: String,
|
||||
tariffCo2: Number,
|
||||
tariffGrid: Number,
|
||||
tariffCo2: { type: Number, default: 0 },
|
||||
tariffGrid: { type: Number, default: 0 },
|
||||
vehicleClimaterActive: Boolean,
|
||||
vehicleWelcomeActive: Boolean,
|
||||
vehicleLimitSoc: Number,
|
||||
vehicleLimitSoc: { type: Number, default: 0 },
|
||||
},
|
||||
emits: ["open-loadpoint-settings", "open-minsoc-settings", "open-plan-modal"],
|
||||
data() {
|
||||
return {
|
||||
pvTooltip: null,
|
||||
phaseTooltip: null,
|
||||
planStartTooltip: null,
|
||||
planActiveTooltip: null,
|
||||
minSocTooltip: null,
|
||||
vehicleClimaterTooltip: null,
|
||||
vehicleWelcomeTooltip: null,
|
||||
smartCostTooltip: null,
|
||||
vehicleLimitTooltip: null,
|
||||
tempLimitTooltip: null,
|
||||
awaitingAuthorizationTooltip: null,
|
||||
disconnectRequiredTooltip: null,
|
||||
batteryBoostTooltip: null,
|
||||
interval: null,
|
||||
planProjectedEndDuration: null,
|
||||
smartCostNextStartDuration: null,
|
||||
planProjectedStartDuration: null,
|
||||
pvTooltip: null as Tooltip | null,
|
||||
phaseTooltip: null as Tooltip | null,
|
||||
planStartTooltip: null as Tooltip | null,
|
||||
planActiveTooltip: null as Tooltip | null,
|
||||
minSocTooltip: null as Tooltip | null,
|
||||
vehicleClimaterTooltip: null as Tooltip | null,
|
||||
vehicleWelcomeTooltip: null as Tooltip | null,
|
||||
smartCostTooltip: null as Tooltip | null,
|
||||
vehicleLimitTooltip: null as Tooltip | null,
|
||||
tempLimitTooltip: null as Tooltip | null,
|
||||
awaitingAuthorizationTooltip: null as Tooltip | null,
|
||||
disconnectRequiredTooltip: null as Tooltip | null,
|
||||
batteryBoostTooltip: null as Tooltip | null,
|
||||
interval: null as Timeout,
|
||||
planProjectedEndDuration: null as string | null,
|
||||
smartCostNextStartDuration: null as string | null,
|
||||
planProjectedStartDuration: null as string | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
phaseTimerActive() {
|
||||
return (
|
||||
this.phaseRemainingInterpolated &&
|
||||
this.phaseRemainingInterpolated > 0 &&
|
||||
["scale1p", "scale3p"].includes(this.phaseAction)
|
||||
);
|
||||
},
|
||||
pvTimerActive() {
|
||||
return (
|
||||
this.pvRemainingInterpolated > 0 && ["enable", "disable"].includes(this.pvAction)
|
||||
this.pvRemainingInterpolated &&
|
||||
this.pvRemainingInterpolated > 0 &&
|
||||
["enable", "disable"].includes(this.pvAction)
|
||||
);
|
||||
},
|
||||
pvTimerVisible() {
|
||||
|
|
@ -492,15 +497,15 @@ export default {
|
|||
return this.$t("main.vehicleStatus.welcome");
|
||||
},
|
||||
chargerStatus() {
|
||||
const t = (key, data) => {
|
||||
const t = (key: string) => {
|
||||
if (this.heating) {
|
||||
// check for special heating status translation
|
||||
const name = `main.heatingStatus.${key}`;
|
||||
if (this.$te(name, DEFAULT_LOCALE)) {
|
||||
return this.$t(name, data);
|
||||
return this.$t(name);
|
||||
}
|
||||
}
|
||||
return this.$t(`main.vehicleStatus.${key}`, data);
|
||||
return this.$t(`main.vehicleStatus.${key}`);
|
||||
};
|
||||
|
||||
if (!this.connected) {
|
||||
|
|
@ -590,7 +595,9 @@ export default {
|
|||
this.updateBatteryBoostTooltip();
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.interval);
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateDurations() {
|
||||
|
|
@ -643,21 +650,21 @@ export default {
|
|||
this.pvTooltip = this.updateTooltip(
|
||||
this.pvTooltip,
|
||||
this.pvTimerContent,
|
||||
this.$refs.pvTimer
|
||||
this.$refs["pvTimer"]
|
||||
);
|
||||
},
|
||||
updatePhaseTooltip() {
|
||||
this.phaseTooltip = this.updateTooltip(
|
||||
this.phaseTooltip,
|
||||
this.phaseTimerContent,
|
||||
this.$refs.phaseTimer
|
||||
this.$refs["phaseTimer"]
|
||||
);
|
||||
},
|
||||
updatePlanStartTooltip() {
|
||||
this.planStartTooltip = this.updateTooltip(
|
||||
this.planStartTooltip,
|
||||
this.planStartTooltipContent,
|
||||
this.$refs.planStart,
|
||||
this.$refs["planStart"],
|
||||
true
|
||||
);
|
||||
},
|
||||
|
|
@ -665,7 +672,7 @@ export default {
|
|||
this.planActiveTooltip = this.updateTooltip(
|
||||
this.planActiveTooltip,
|
||||
this.planActiveTooltipContent,
|
||||
this.$refs.planActive,
|
||||
this.$refs["planActive"],
|
||||
true
|
||||
);
|
||||
},
|
||||
|
|
@ -673,7 +680,7 @@ export default {
|
|||
this.minSocTooltip = this.updateTooltip(
|
||||
this.minSocTooltip,
|
||||
this.minSocTooltipContent,
|
||||
this.$refs.minSoc,
|
||||
this.$refs["minSoc"],
|
||||
true
|
||||
);
|
||||
},
|
||||
|
|
@ -681,28 +688,28 @@ export default {
|
|||
this.vehicleClimaterTooltip = this.updateTooltip(
|
||||
this.vehicleClimaterTooltip,
|
||||
this.vehicleClimaterTooltipContent,
|
||||
this.$refs.vehicleClimater
|
||||
this.$refs["vehicleClimater"]
|
||||
);
|
||||
},
|
||||
updateBatteryBoostTooltip() {
|
||||
this.batteryBoostTooltip = this.updateTooltip(
|
||||
this.batteryBoostTooltip,
|
||||
this.batteryBoostTooltipContent,
|
||||
this.$refs.batteryBoost
|
||||
this.$refs["batteryBoost"]
|
||||
);
|
||||
},
|
||||
updateVehicleWelcomeTooltip() {
|
||||
this.vehicleWelcomeTooltip = this.updateTooltip(
|
||||
this.vehicleWelcomeTooltip,
|
||||
this.vehicleWelcomeTooltipContent,
|
||||
this.$refs.vehicleWelcome
|
||||
this.$refs["vehicleWelcome"]
|
||||
);
|
||||
},
|
||||
updateSmartCostTooltip() {
|
||||
this.smartCostTooltip = this.updateTooltip(
|
||||
this.smartCostTooltip,
|
||||
this.smartCostTooltipContent,
|
||||
this.$refs.smartCost,
|
||||
this.$refs["smartCost"],
|
||||
true
|
||||
);
|
||||
},
|
||||
|
|
@ -710,7 +717,7 @@ export default {
|
|||
this.vehicleLimitTooltip = this.updateTooltip(
|
||||
this.vehicleLimitTooltip,
|
||||
this.vehicleLimitTooltipContent,
|
||||
this.$refs.vehicleLimit,
|
||||
this.$refs["vehicleLimit"],
|
||||
true
|
||||
);
|
||||
},
|
||||
|
|
@ -718,29 +725,34 @@ export default {
|
|||
this.tempLimitTooltip = this.updateTooltip(
|
||||
this.tempLimitTooltip,
|
||||
this.tempLimitTooltipContent,
|
||||
this.$refs.tempLimit
|
||||
this.$refs["tempLimit"]
|
||||
);
|
||||
},
|
||||
updateAwaitingAuthorizationTooltip() {
|
||||
this.awaitingAuthorizationTooltip = this.updateTooltip(
|
||||
this.awaitingAuthorizationTooltip,
|
||||
this.awaitingAuthorizationTooltipContent,
|
||||
this.$refs.awaitingAuthorization
|
||||
this.$refs["awaitingAuthorization"]
|
||||
);
|
||||
},
|
||||
updateDisconnectRequiredTooltip() {
|
||||
this.updateTooltip(
|
||||
this.disconnectRequiredTooltip = this.updateTooltip(
|
||||
this.disconnectRequiredTooltip,
|
||||
this.disconnectRequiredTooltipContent,
|
||||
this.$refs.disconnectRequired
|
||||
this.$refs["disconnectRequired"]
|
||||
);
|
||||
},
|
||||
updateTooltip(instance, content, ref, hoverOnly = false) {
|
||||
updateTooltip(
|
||||
instance: Tooltip | null,
|
||||
content: string,
|
||||
ref: HTMLElement | undefined,
|
||||
hoverOnly = false
|
||||
): Tooltip | null {
|
||||
if (!content || !ref) {
|
||||
if (instance) {
|
||||
instance.dispose();
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
let newInstance = instance;
|
||||
if (!newInstance) {
|
||||
|
|
@ -751,7 +763,7 @@ export default {
|
|||
return newInstance;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import "@h2d2/shopicons/es/regular/cablecharge";
|
||||
import Tooltip from "bootstrap/js/dist/tooltip";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
|
|
@ -60,8 +60,10 @@ import Options from "./Options.vue";
|
|||
import CloudOffline from "../MaterialIcon/CloudOffline.vue";
|
||||
import Sync from "../MaterialIcon/Sync.vue";
|
||||
import collector from "@/mixins/collector";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { SelectOption, Vehicle } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "VehicleTitle",
|
||||
components: { VehicleOptions: Options, VehicleIcon, Sync, CloudOffline },
|
||||
mixins: [collector],
|
||||
|
|
@ -72,14 +74,14 @@ export default {
|
|||
vehicleNotReachable: Boolean,
|
||||
icon: String,
|
||||
vehicleName: String,
|
||||
vehicles: { type: Array, default: () => [] },
|
||||
vehicles: { type: Array as PropType<Vehicle[]>, default: () => [] },
|
||||
title: String,
|
||||
},
|
||||
emits: ["change-vehicle", "remove-vehicle"],
|
||||
data() {
|
||||
return {
|
||||
refreshTooltip: null,
|
||||
notReachableTooltip: null,
|
||||
refreshTooltip: null as Tooltip | null,
|
||||
notReachableTooltip: null as Tooltip | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -101,6 +103,12 @@ export default {
|
|||
}
|
||||
return this.$t("main.vehicle.none");
|
||||
},
|
||||
vehicleOptions(): SelectOption<string>[] {
|
||||
return this.vehicles.map((v) => ({
|
||||
name: v.name,
|
||||
value: v.title,
|
||||
}));
|
||||
},
|
||||
vehicleKnown() {
|
||||
return !!this.vehicleName;
|
||||
},
|
||||
|
|
@ -120,7 +128,7 @@ export default {
|
|||
this.initTooltip();
|
||||
},
|
||||
methods: {
|
||||
changeVehicle(name) {
|
||||
changeVehicle(name: string) {
|
||||
this.$emit("change-vehicle", name);
|
||||
},
|
||||
removeVehicle() {
|
||||
|
|
@ -130,21 +138,23 @@ export default {
|
|||
this.$nextTick(() => {
|
||||
this.refreshTooltip?.dispose();
|
||||
this.notReachableTooltip?.dispose();
|
||||
if (this.$refs.refresh) {
|
||||
this.refreshTooltip = new Tooltip(this.$refs.refresh);
|
||||
if (this.$refs["refresh"]) {
|
||||
this.refreshTooltip = new Tooltip(this.$refs["refresh"]);
|
||||
}
|
||||
if (this.$refs.notReachable) {
|
||||
this.notReachableTooltip = new Tooltip(this.$refs.notReachable);
|
||||
if (this.$refs["notReachable"]) {
|
||||
this.notReachableTooltip = new Tooltip(this.$refs["notReachable"]);
|
||||
}
|
||||
});
|
||||
},
|
||||
openHelpModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("helpModal"));
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("helpModal") as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
this.initTooltip();
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
import Vehicle from "./Vehicle.vue";
|
||||
|
||||
const hoursFromNow = (hours) => {
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + hours);
|
||||
return now.toISOString();
|
||||
};
|
||||
|
||||
const baseState = {
|
||||
vehicle: { title: "Mein Auto", icon: "car", capacity: 72, features: [] },
|
||||
enabled: false,
|
||||
connected: true,
|
||||
vehicleName: "meinauto",
|
||||
vehicleSoc: 42.742,
|
||||
vehicleRange: 231,
|
||||
limitSoc: 90,
|
||||
chargedEnergy: 14123,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Vehicles/Vehicle",
|
||||
component: Vehicle,
|
||||
argTypes: {
|
||||
chargedEnergy: { control: "number" },
|
||||
charging: { control: "boolean" },
|
||||
vehicleClimaterActive: { control: "boolean" },
|
||||
vehicleWelcomeActive: { control: "boolean" },
|
||||
connected: { control: "boolean" },
|
||||
currency: { control: "text" },
|
||||
effectiveLimitSoc: { control: "number" },
|
||||
effectivePlanSoc: { control: "number" },
|
||||
effectivePlanTime: { control: "text" },
|
||||
batteryBoostActive: { control: "boolean" },
|
||||
enabled: { control: "boolean" },
|
||||
heating: { control: "boolean" },
|
||||
id: { control: "text" },
|
||||
integratedDevice: { control: "boolean" },
|
||||
limitEnergy: { control: "number" },
|
||||
mode: { control: "text" },
|
||||
chargerStatusReason: { control: "text" },
|
||||
vehicle: { control: "object" },
|
||||
vehicleName: { control: "text" },
|
||||
vehicleRange: { control: "number" },
|
||||
vehicleSoc: { control: "number" },
|
||||
vehicleLimitSoc: { control: "number" },
|
||||
socBasedCharging: { control: "boolean" },
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => ({
|
||||
components: { Vehicle },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Vehicle v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Disconnected = Template.bind({});
|
||||
Disconnected.args = {
|
||||
...baseState,
|
||||
connected: false,
|
||||
};
|
||||
|
||||
export const Connected = Template.bind({});
|
||||
Connected.args = {
|
||||
...baseState,
|
||||
};
|
||||
|
||||
export const Ready = Template.bind({});
|
||||
Ready.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export const Charging = Template.bind({});
|
||||
Charging.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
};
|
||||
|
||||
export const UnknownVehicle = Template.bind({});
|
||||
UnknownVehicle.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleName: "",
|
||||
socBasedCharging: false,
|
||||
vehicle: { ...baseState.vehicle, capacity: null },
|
||||
mode: "pv",
|
||||
};
|
||||
|
||||
export const OfflineVehicle = Template.bind({});
|
||||
OfflineVehicle.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
mode: "pv",
|
||||
};
|
||||
|
||||
export const OfflineVehicleWithTarget = Template.bind({});
|
||||
OfflineVehicleWithTarget.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
targetEnergy: 30,
|
||||
mode: "pv",
|
||||
};
|
||||
|
||||
export const VehicleLimit = Template.bind({});
|
||||
VehicleLimit.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleLimitSoc: 80,
|
||||
};
|
||||
|
||||
export const VehicleLimitReached = Template.bind({});
|
||||
VehicleLimitReached.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 80,
|
||||
vehicleSoc: 80,
|
||||
};
|
||||
|
||||
export const TargetChargePlanned = Template.bind({});
|
||||
TargetChargePlanned.args = {
|
||||
...baseState,
|
||||
targetTime: hoursFromNow(34),
|
||||
mode: "pv",
|
||||
};
|
||||
|
||||
export const TargetChargeActive = Template.bind({});
|
||||
TargetChargeActive.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
targetTime: hoursFromNow(14),
|
||||
mode: "pv",
|
||||
};
|
||||
|
||||
export const SmartChargeCostLimitActive = Template.bind({});
|
||||
SmartChargeCostLimitActive.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
smartCostLimit: 0.13,
|
||||
mode: "pv",
|
||||
};
|
||||
|
||||
export const PvEnableTimer = Template.bind({});
|
||||
PvEnableTimer.args = {
|
||||
...baseState,
|
||||
pvAction: "enable",
|
||||
pvRemainingInterpolated: 32,
|
||||
};
|
||||
|
||||
export const PvDisableTimer = Template.bind({});
|
||||
PvDisableTimer.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
pvAction: "disable",
|
||||
pvRemainingInterpolated: 155,
|
||||
};
|
||||
|
||||
export const VehicleSwitch = Template.bind({});
|
||||
VehicleSwitch.args = {
|
||||
...baseState,
|
||||
vehicleTitle: "Blauer e-Golf",
|
||||
vehicles: ["Blauer e-Golf", "Weißes Model 3"],
|
||||
};
|
||||
187
assets/js/components/Vehicles/Vehicle.stories.ts
Normal file
187
assets/js/components/Vehicles/Vehicle.stories.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import { CHARGE_MODE } from "@/types/evcc";
|
||||
import Vehicle from "./Vehicle.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
|
||||
const baseState = {
|
||||
vehicle: {
|
||||
title: "Mein Auto",
|
||||
icon: "car",
|
||||
capacity: 72,
|
||||
features: [],
|
||||
name: "",
|
||||
repeatingPlans: [],
|
||||
},
|
||||
enabled: false,
|
||||
connected: true,
|
||||
vehicleName: "meinauto",
|
||||
vehicleSoc: 42.742,
|
||||
vehicleRange: 231,
|
||||
limitSoc: 90,
|
||||
chargedEnergy: 14123,
|
||||
socBasedCharging: true,
|
||||
id: 0,
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Vehicles/Vehicle",
|
||||
component: Vehicle,
|
||||
argTypes: {
|
||||
chargedEnergy: { control: "number" },
|
||||
charging: { control: "boolean" },
|
||||
vehicleClimaterActive: { control: "boolean" },
|
||||
vehicleWelcomeActive: { control: "boolean" },
|
||||
connected: { control: "boolean" },
|
||||
currency: { control: "text" },
|
||||
effectiveLimitSoc: { control: "number" },
|
||||
effectivePlanSoc: { control: "number" },
|
||||
effectivePlanTime: { control: "text" },
|
||||
batteryBoostActive: { control: "boolean" },
|
||||
enabled: { control: "boolean" },
|
||||
heating: { control: "boolean" },
|
||||
id: { control: "text" },
|
||||
integratedDevice: { control: "boolean" },
|
||||
limitEnergy: { control: "number" },
|
||||
mode: { control: "text" },
|
||||
chargerStatusReason: { control: "text" },
|
||||
vehicle: { control: "object" },
|
||||
vehicleName: { control: "text" },
|
||||
vehicleRange: { control: "number" },
|
||||
vehicleSoc: { control: "number" },
|
||||
vehicleLimitSoc: { control: "number" },
|
||||
socBasedCharging: { control: "boolean" },
|
||||
},
|
||||
} as Meta<typeof Vehicle>;
|
||||
|
||||
const Template: StoryFn<typeof Vehicle> = (args) => ({
|
||||
components: { Vehicle },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: '<Vehicle v-bind="args" />',
|
||||
});
|
||||
|
||||
export const Disconnected = Template.bind({});
|
||||
Disconnected.args = {
|
||||
...baseState,
|
||||
connected: false,
|
||||
};
|
||||
|
||||
export const Connected = Template.bind({});
|
||||
Connected.args = {
|
||||
...baseState,
|
||||
};
|
||||
|
||||
export const Ready = Template.bind({});
|
||||
Ready.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export const Charging = Template.bind({});
|
||||
Charging.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
};
|
||||
|
||||
export const UnknownVehicle = Template.bind({});
|
||||
UnknownVehicle.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleName: "",
|
||||
socBasedCharging: false,
|
||||
vehicle: { ...baseState.vehicle, capacity: undefined },
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const OfflineVehicle = Template.bind({});
|
||||
OfflineVehicle.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const OfflineVehicleWithTarget = Template.bind({});
|
||||
OfflineVehicleWithTarget.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
socBasedCharging: false,
|
||||
vehicle: {
|
||||
...baseState.vehicle,
|
||||
title: "Opel Corsa-e",
|
||||
capacity: 72,
|
||||
features: ["Offline"],
|
||||
},
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const VehicleLimit = Template.bind({});
|
||||
VehicleLimit.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
vehicleLimitSoc: 80,
|
||||
};
|
||||
|
||||
export const VehicleLimitReached = Template.bind({});
|
||||
VehicleLimitReached.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
vehicleLimitSoc: 80,
|
||||
vehicleSoc: 80,
|
||||
};
|
||||
|
||||
export const TargetChargePlanned = Template.bind({});
|
||||
TargetChargePlanned.args = {
|
||||
...baseState,
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const TargetChargeActive = Template.bind({});
|
||||
TargetChargeActive.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const SmartChargeCostLimitActive = Template.bind({});
|
||||
SmartChargeCostLimitActive.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
smartCostLimit: 0.13,
|
||||
mode: CHARGE_MODE.PV,
|
||||
};
|
||||
|
||||
export const PvEnableTimer = Template.bind({});
|
||||
PvEnableTimer.args = {
|
||||
...baseState,
|
||||
pvAction: "enable",
|
||||
pvRemainingInterpolated: 32,
|
||||
};
|
||||
|
||||
export const PvDisableTimer = Template.bind({});
|
||||
PvDisableTimer.args = {
|
||||
...baseState,
|
||||
enabled: true,
|
||||
charging: true,
|
||||
pvAction: "disable",
|
||||
pvRemainingInterpolated: 155,
|
||||
};
|
||||
|
||||
export const VehicleSwitch = Template.bind({});
|
||||
VehicleSwitch.args = {
|
||||
...baseState,
|
||||
vehicles: ["Blauer e-Golf", "Weißes Model 3"],
|
||||
};
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import collector from "@/mixins/collector";
|
||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||
|
|
@ -78,8 +78,10 @@ import ChargingPlan from "../ChargingPlans/ChargingPlan.vue";
|
|||
import LimitSocSelect from "./LimitSocSelect.vue";
|
||||
import LimitEnergySelect from "./LimitEnergySelect.vue";
|
||||
import { distanceUnit, distanceValue } from "@/units";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { CHARGE_MODE, type Forecast, type Vehicle } from "@/types/evcc";
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: "Vehicle",
|
||||
components: {
|
||||
VehicleTitle: Title,
|
||||
|
|
@ -92,7 +94,7 @@ export default {
|
|||
},
|
||||
mixins: [collector, formatter],
|
||||
props: {
|
||||
chargedEnergy: Number,
|
||||
chargedEnergy: { type: Number, default: 0 },
|
||||
charging: Boolean,
|
||||
vehicleClimaterActive: Boolean,
|
||||
vehicleWelcomeActive: Boolean,
|
||||
|
|
@ -107,11 +109,11 @@ export default {
|
|||
id: [String, Number],
|
||||
integratedDevice: Boolean,
|
||||
limitEnergy: Number,
|
||||
mode: String,
|
||||
mode: String as PropType<CHARGE_MODE>,
|
||||
chargerStatusReason: String,
|
||||
phaseAction: String,
|
||||
phaseRemainingInterpolated: Number,
|
||||
forecast: Object, // as PropType<Forecast>,
|
||||
forecast: Object as PropType<Forecast>,
|
||||
planActive: Boolean,
|
||||
planEnergy: Number,
|
||||
planProjectedStart: String,
|
||||
|
|
@ -131,12 +133,12 @@ export default {
|
|||
socBasedPlanning: Boolean,
|
||||
tariffCo2: Number,
|
||||
tariffGrid: Number,
|
||||
vehicle: Object,
|
||||
vehicle: Object as PropType<Vehicle>,
|
||||
vehicleDetectionActive: Boolean,
|
||||
vehicleName: String,
|
||||
vehicleRange: Number,
|
||||
vehicles: Array,
|
||||
vehicleSoc: Number,
|
||||
vehicleSoc: { type: Number, default: 0 },
|
||||
vehicleLimitSoc: Number,
|
||||
vehicleNotReachable: Boolean,
|
||||
},
|
||||
|
|
@ -202,23 +204,23 @@ export default {
|
|||
if (this.vehicleSoc > 10 && this.range) {
|
||||
return Math.round((this.range / this.vehicleSoc) * 1e2) / 1e2;
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
},
|
||||
socPerKwh() {
|
||||
if (this.capacity > 0) {
|
||||
return 100 / this.capacity;
|
||||
}
|
||||
return null;
|
||||
return 0;
|
||||
},
|
||||
chargedSoc() {
|
||||
const value = this.socPerKwh * (this.chargedEnergy / 1e3);
|
||||
return value > 1 ? `+${this.fmtPercentage(value)}` : null;
|
||||
},
|
||||
chargingPlanDisabled() {
|
||||
return ["off", "now"].includes(this.mode);
|
||||
return this.mode && [CHARGE_MODE.OFF, CHARGE_MODE.NOW].includes(this.mode);
|
||||
},
|
||||
smartCostDisabled() {
|
||||
return ["off", "now"].includes(this.mode);
|
||||
return this.chargingPlanDisabled;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -227,33 +229,37 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
limitSocDrag(limitSoc) {
|
||||
limitSocDrag(limitSoc: number) {
|
||||
this.displayLimitSoc = limitSoc;
|
||||
},
|
||||
limitSocUpdated(limitSoc) {
|
||||
limitSocUpdated(limitSoc: number) {
|
||||
this.displayLimitSoc = limitSoc;
|
||||
this.$emit("limit-soc-updated", limitSoc);
|
||||
},
|
||||
limitEnergyUpdated(limitEnergy) {
|
||||
limitEnergyUpdated(limitEnergy: number) {
|
||||
this.$emit("limit-energy-updated", limitEnergy);
|
||||
},
|
||||
changeVehicle(name) {
|
||||
changeVehicle(name: string) {
|
||||
this.$emit("change-vehicle", name);
|
||||
},
|
||||
removeVehicle() {
|
||||
this.$emit("remove-vehicle");
|
||||
},
|
||||
fmtEnergy(value) {
|
||||
fmtEnergy(value: number) {
|
||||
return this.fmtWh(value, value == 0 ? POWER_UNIT.KW : POWER_UNIT.AUTO);
|
||||
},
|
||||
openPlanModal() {
|
||||
this.$refs.chargingPlan.openPlanModal();
|
||||
(
|
||||
this.$refs["chargingPlan"] as InstanceType<typeof ChargingPlan> | undefined
|
||||
)?.openPlanModal();
|
||||
},
|
||||
openMinSocSettings() {
|
||||
this.$refs.chargingPlan.openPlanModal(true);
|
||||
(
|
||||
this.$refs["chargingPlan"] as InstanceType<typeof ChargingPlan> | undefined
|
||||
)?.openPlanModal(true);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ export interface Vehicle {
|
|||
title: string;
|
||||
features?: string[];
|
||||
capacity?: number;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export type Timeout = ReturnType<typeof setInterval> | null;
|
||||
|
|
|
|||
|
|
@ -288,3 +288,45 @@ test.describe("csv export", async () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("session details", async () => {
|
||||
test("show session details (session 5)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=5");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByTestId("session-details").getByRole("heading", { name: "Charging Session" })
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Garage");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("weißes Model 3");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Thu, May 4, 22:00", "Fri, May 5, 06:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("5.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("0.0% (0.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.50 € 50.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-odometer")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-meter")).toHaveCount(0);
|
||||
await expect(page.getByTestId("session-details-delete")).toContainText("Delete");
|
||||
});
|
||||
|
||||
test("show session details with CO2 data (session 1)", async ({ page }) => {
|
||||
await page.goto("/#/sessions?year=2023&month=3");
|
||||
await page.getByTestId("sessions-entry").nth(0).click();
|
||||
await expect(page.getByTestId("session-details")).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("session-details-loadpoint")).toContainText("Carport");
|
||||
await expect(page.getByTestId("session-details-vehicle")).toContainText("blauer e-Golf");
|
||||
await expect(page.getByTestId("session-details-date")).toContainText(
|
||||
["Wed, Mar 1, 07:00", "Tue, May 2, 12:00"].join("")
|
||||
);
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("10.0 kWh");
|
||||
await expect(page.getByTestId("session-details-energy")).toContainText("1:00");
|
||||
await expect(page.getByTestId("session-details-solar")).toContainText("100.0% (10.0 kWh)");
|
||||
await expect(page.getByTestId("session-details-price")).toContainText("2.00 € 20.0 ct/kWh");
|
||||
await expect(page.getByTestId("session-details-co2")).toContainText("300 g/kWh");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue