refactor: use single modal instances for loadpoint (#29109)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
d78491a07b
commit
3f19b6a6de
14 changed files with 451 additions and 343 deletions
|
|
@ -28,93 +28,26 @@
|
|||
</button>
|
||||
</div>
|
||||
</LabelAndValue>
|
||||
|
||||
<Teleport to="body">
|
||||
<GenericModal
|
||||
:id="modalId"
|
||||
ref="modal"
|
||||
:title="modalTitle"
|
||||
size="xl"
|
||||
data-testid="charging-plan-modal"
|
||||
@open="modalVisible"
|
||||
@closed="modalInvisible"
|
||||
>
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{ active: departureTabActive }"
|
||||
href="#"
|
||||
@click.prevent="showDeatureTab"
|
||||
>
|
||||
{{ $t("main.chargingPlan.departureTab") }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{ active: arrivalTabActive }"
|
||||
href="#"
|
||||
@click.prevent="showArrivalTab"
|
||||
>
|
||||
{{ $t("main.chargingPlan.arrivalTab") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="isModalVisible">
|
||||
<ChargingPlansSettings
|
||||
v-if="departureTabActive"
|
||||
v-bind="chargingPlansSettingsProps"
|
||||
@static-plan-updated="updateStaticPlan"
|
||||
@static-plan-removed="removeStaticPlan"
|
||||
@repeating-plans-updated="updateRepeatingPlans"
|
||||
@plan-strategy-updated="updatePlanStrategy"
|
||||
/>
|
||||
<ChargingPlanArrival
|
||||
v-if="arrivalTabActive"
|
||||
v-bind="chargingPlanArrival"
|
||||
@minsoc-updated="setMinSoc"
|
||||
@limitsoc-updated="setLimitSoc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||
import PlansSettings from "./PlansSettings.vue";
|
||||
import Arrival from "./Arrival.vue";
|
||||
|
||||
import formatter from "@/mixins/formatter";
|
||||
import collector from "@/mixins/collector";
|
||||
import minuteTicker from "@/mixins/minuteTicker";
|
||||
import api from "@/api";
|
||||
import { optionStep, fmtEnergy } from "@/utils/energyOptions.ts";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { CURRENCY, Vehicle } from "@/types/evcc";
|
||||
import type {
|
||||
StaticPlan,
|
||||
StaticSocPlan,
|
||||
StaticEnergyPlan,
|
||||
RepeatingPlan,
|
||||
PlanStrategy,
|
||||
} from "./types";
|
||||
import type { PlanStrategy } from "./types";
|
||||
import type { Forecast } from "@/types/evcc.ts";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChargingPlan",
|
||||
components: {
|
||||
GenericModal,
|
||||
LabelAndValue,
|
||||
ChargingPlansSettings: PlansSettings,
|
||||
ChargingPlanArrival: Arrival,
|
||||
},
|
||||
mixins: [formatter, collector, minuteTicker],
|
||||
mixins: [formatter, minuteTicker],
|
||||
props: {
|
||||
currency: String as PropType<CURRENCY>,
|
||||
disabled: Boolean,
|
||||
|
|
@ -142,10 +75,9 @@ export default defineComponent({
|
|||
vehicleNotReachable: Boolean,
|
||||
forecast: Object as PropType<Forecast>,
|
||||
},
|
||||
emits: ["open-modal"],
|
||||
data() {
|
||||
return {
|
||||
isModalVisible: false,
|
||||
activeTab: "departure",
|
||||
targetTimeLabel: "",
|
||||
};
|
||||
},
|
||||
|
|
@ -165,60 +97,9 @@ export default defineComponent({
|
|||
limitSoc(): number | undefined {
|
||||
return this.vehicle?.limitSoc;
|
||||
},
|
||||
staticPlan(): StaticPlan | null {
|
||||
if (this.socBasedPlanning) {
|
||||
const plan = this.vehicle?.plan as StaticSocPlan;
|
||||
if (plan) {
|
||||
return {
|
||||
soc: plan.soc,
|
||||
time: new Date(plan.time),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (this.planEnergy && this.planTime) {
|
||||
return {
|
||||
energy: this.planEnergy,
|
||||
time: new Date(this.planTime),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
repeatingPlans(): RepeatingPlan[] {
|
||||
if (
|
||||
this.vehicle &&
|
||||
this.vehicle.repeatingPlans &&
|
||||
this.vehicle.repeatingPlans.length > 0
|
||||
) {
|
||||
return [...(this.vehicle.repeatingPlans || [])];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
enabled(): boolean {
|
||||
return !!this.effectivePlanTime;
|
||||
},
|
||||
modalId(): string {
|
||||
return `chargingPlanModal_${this.id}`;
|
||||
},
|
||||
modalTitle(): string {
|
||||
const baseTitle = this.$t("main.chargingPlan.modalTitle");
|
||||
if (this.socBasedPlanning && this.vehicle) {
|
||||
return `${baseTitle}: ${this.vehicle.title}`;
|
||||
}
|
||||
return baseTitle;
|
||||
},
|
||||
departureTabActive(): boolean {
|
||||
return this.activeTab === "departure";
|
||||
},
|
||||
arrivalTabActive(): boolean {
|
||||
return this.activeTab === "arrival";
|
||||
},
|
||||
chargingPlansSettingsProps(): any {
|
||||
return this.collectProps(PlansSettings);
|
||||
},
|
||||
chargingPlanArrival(): any {
|
||||
return this.collectProps(Arrival);
|
||||
},
|
||||
targetSocLabel(): string {
|
||||
if (this.socBasedPlanning && this.effectivePlanSoc) {
|
||||
return this.fmtPercentage(this.effectivePlanSoc);
|
||||
|
|
@ -230,12 +111,6 @@ export default defineComponent({
|
|||
this.$t("main.targetEnergy.noLimit")
|
||||
);
|
||||
},
|
||||
apiVehicle(): string {
|
||||
return `vehicles/${this.vehicle?.name}/`;
|
||||
},
|
||||
apiLoadpoint(): string {
|
||||
return `loadpoints/${this.id}/`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
effectivePlanTime(): void {
|
||||
|
|
@ -254,70 +129,14 @@ export default defineComponent({
|
|||
this.updateTargetTimeLabel();
|
||||
},
|
||||
methods: {
|
||||
modalVisible(): void {
|
||||
this.isModalVisible = true;
|
||||
},
|
||||
modalInvisible(): void {
|
||||
this.isModalVisible = false;
|
||||
},
|
||||
openModal(): void {
|
||||
this.showDeatureTab();
|
||||
const modalRef = this.$refs["modal"] as any;
|
||||
modalRef?.open();
|
||||
},
|
||||
openPlanModal(arrivalTab = false) {
|
||||
if (arrivalTab) {
|
||||
this.showArrivalTab();
|
||||
} else {
|
||||
this.showDeatureTab();
|
||||
}
|
||||
const modalRef = this.$refs["modal"] as any;
|
||||
modalRef?.open();
|
||||
this.$emit("open-modal");
|
||||
},
|
||||
updateTargetTimeLabel(): void {
|
||||
if (!this.effectivePlanTime) return;
|
||||
const targetDate = new Date(this.effectivePlanTime);
|
||||
this.targetTimeLabel = this.fmtAbsoluteDate(targetDate);
|
||||
},
|
||||
showDeatureTab(): void {
|
||||
this.activeTab = "departure";
|
||||
},
|
||||
showArrivalTab(): void {
|
||||
this.activeTab = "arrival";
|
||||
},
|
||||
updateStaticPlan(plan: StaticPlan): void {
|
||||
const timeISO = plan.time.toISOString();
|
||||
if (this.socBasedPlanning) {
|
||||
const p = plan as StaticSocPlan;
|
||||
api.post(`${this.apiVehicle}plan/soc/${p.soc}/${timeISO}`, null);
|
||||
} else {
|
||||
const p = plan as StaticEnergyPlan;
|
||||
api.post(`${this.apiLoadpoint}plan/energy/${p.energy}/${timeISO}`, null);
|
||||
}
|
||||
},
|
||||
removeStaticPlan(): void {
|
||||
if (this.socBasedPlanning) {
|
||||
api.delete(`${this.apiVehicle}plan/soc`);
|
||||
} else {
|
||||
api.delete(`${this.apiLoadpoint}plan/energy`);
|
||||
}
|
||||
},
|
||||
updateRepeatingPlans(plans: RepeatingPlan[]): void {
|
||||
api.post(`${this.apiVehicle}plan/repeating`, plans);
|
||||
},
|
||||
updatePlanStrategy(strategy: PlanStrategy): void {
|
||||
if (this.socBasedPlanning) {
|
||||
api.post(`${this.apiVehicle}plan/strategy`, strategy);
|
||||
} else {
|
||||
api.post(`${this.apiLoadpoint}plan/strategy`, strategy);
|
||||
}
|
||||
},
|
||||
setMinSoc(soc: number): void {
|
||||
api.post(`${this.apiVehicle}minsoc/${soc}`);
|
||||
},
|
||||
setLimitSoc(soc: number): void {
|
||||
api.post(`${this.apiVehicle}limitsoc/${soc}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
226
assets/js/components/ChargingPlans/ChargingPlanModal.vue
Normal file
226
assets/js/components/ChargingPlans/ChargingPlanModal.vue
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<GenericModal
|
||||
id="chargingPlanModal"
|
||||
ref="modal"
|
||||
:title="modalTitle"
|
||||
size="xl"
|
||||
data-testid="charging-plan-modal"
|
||||
@open="modalVisible"
|
||||
@closed="modalInvisible"
|
||||
>
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{ active: departureTabActive }"
|
||||
href="#"
|
||||
@click.prevent="showDepartureTab"
|
||||
>
|
||||
{{ $t("main.chargingPlan.departureTab") }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{ active: arrivalTabActive }"
|
||||
href="#"
|
||||
@click.prevent="showArrivalTab"
|
||||
>
|
||||
{{ $t("main.chargingPlan.arrivalTab") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="isModalVisible">
|
||||
<PlansSettings
|
||||
v-if="departureTabActive"
|
||||
:id="id"
|
||||
:staticPlan="staticPlan"
|
||||
:repeatingPlans="repeatingPlans"
|
||||
:effectiveLimitSoc="loadpoint?.effectiveLimitSoc"
|
||||
:effectivePlanTime="loadpoint?.effectivePlanTime ?? undefined"
|
||||
:effectivePlanSoc="loadpoint?.effectivePlanSoc"
|
||||
:effectivePlanStrategy="loadpoint?.effectivePlanStrategy"
|
||||
:planEnergy="loadpoint?.planEnergy"
|
||||
:limitEnergy="loadpoint?.limitEnergy"
|
||||
:socBasedPlanning="!!loadpoint?.socBasedPlanning"
|
||||
:socPerKwh="loadpoint?.socPerKwh"
|
||||
:rangePerSoc="loadpoint?.rangePerSoc"
|
||||
:smartCostType="smartCostType"
|
||||
:currency="currency"
|
||||
:mode="loadpoint?.mode"
|
||||
:capacity="vehicle?.capacity"
|
||||
:vehicle="vehicle"
|
||||
:vehicleLimitSoc="loadpoint?.vehicleLimitSoc"
|
||||
:planOverrun="loadpoint?.planOverrun"
|
||||
:forecast="forecast"
|
||||
@static-plan-updated="updateStaticPlan"
|
||||
@static-plan-removed="removeStaticPlan"
|
||||
@repeating-plans-updated="updateRepeatingPlans"
|
||||
@plan-strategy-updated="updatePlanStrategy"
|
||||
/>
|
||||
<Arrival
|
||||
v-if="arrivalTabActive"
|
||||
:id="id"
|
||||
:minSoc="vehicle?.minSoc"
|
||||
:limitSoc="vehicle?.limitSoc"
|
||||
:vehicleName="vehicle?.name"
|
||||
:vehicleNotReachable="loadpoint?.vehicleNotReachable"
|
||||
:socBasedCharging="loadpoint?.socBasedCharging"
|
||||
:rangePerSoc="loadpoint?.rangePerSoc"
|
||||
@minsoc-updated="setMinSoc"
|
||||
@limitsoc-updated="setLimitSoc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import PlansSettings from "./PlansSettings.vue";
|
||||
import Arrival from "./Arrival.vue";
|
||||
import api from "@/api";
|
||||
import type {
|
||||
PlanStrategy,
|
||||
RepeatingPlan,
|
||||
StaticEnergyPlan,
|
||||
StaticPlan,
|
||||
StaticSocPlan,
|
||||
} from "./types";
|
||||
import type { CURRENCY, Forecast, SMART_COST_TYPE, UiLoadpoint, Vehicle } from "@/types/evcc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChargingPlanModal",
|
||||
components: {
|
||||
GenericModal,
|
||||
PlansSettings,
|
||||
Arrival,
|
||||
},
|
||||
props: {
|
||||
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||
vehicles: { type: Array as PropType<Vehicle[]>, default: () => [] },
|
||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||
currency: String as PropType<CURRENCY>,
|
||||
forecast: Object as PropType<Forecast>,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isModalVisible: false,
|
||||
activeTab: "departure",
|
||||
id: undefined as string | number | undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
staticPlan(): StaticPlan | undefined {
|
||||
if (this.loadpoint?.socBasedPlanning) {
|
||||
const plan = this.vehicle?.plan as StaticSocPlan;
|
||||
if (plan) {
|
||||
return {
|
||||
soc: plan.soc,
|
||||
time: new Date(plan.time),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (this.loadpoint?.planEnergy && this.loadpoint?.planTime) {
|
||||
return {
|
||||
energy: this.loadpoint.planEnergy,
|
||||
time: new Date(this.loadpoint.planTime),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
loadpoint() {
|
||||
return this.loadpoints.find((loadpoint) => loadpoint.id === this.id);
|
||||
},
|
||||
vehicle() {
|
||||
return this.vehicles?.find((v) => v.name === this.loadpoint?.vehicleName);
|
||||
},
|
||||
modalTitle(): string {
|
||||
const baseTitle = this.$t("main.chargingPlan.modalTitle");
|
||||
if (this.loadpoint?.socBasedPlanning && this.vehicle) {
|
||||
return `${baseTitle}: ${this.vehicle.title}`;
|
||||
}
|
||||
return baseTitle;
|
||||
},
|
||||
departureTabActive(): boolean {
|
||||
return this.activeTab === "departure";
|
||||
},
|
||||
arrivalTabActive(): boolean {
|
||||
return this.activeTab === "arrival";
|
||||
},
|
||||
apiVehicle(): string {
|
||||
return `vehicles/${this.vehicle?.name}/`;
|
||||
},
|
||||
apiLoadpoint(): string {
|
||||
return `loadpoints/${this.id}/`;
|
||||
},
|
||||
repeatingPlans(): RepeatingPlan[] {
|
||||
if (
|
||||
this.vehicle &&
|
||||
this.vehicle.repeatingPlans &&
|
||||
this.vehicle.repeatingPlans.length > 0
|
||||
) {
|
||||
return this.vehicle.repeatingPlans || [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open(loadpointId?: string | number) {
|
||||
this.id = loadpointId;
|
||||
const modalRef = this.$refs["modal"] as InstanceType<typeof GenericModal> | undefined;
|
||||
modalRef?.open();
|
||||
},
|
||||
modalVisible(): void {
|
||||
this.isModalVisible = true;
|
||||
},
|
||||
modalInvisible(): void {
|
||||
this.isModalVisible = false;
|
||||
},
|
||||
setMinSoc(soc: number): void {
|
||||
api.post(`${this.apiVehicle}minsoc/${soc}`);
|
||||
},
|
||||
setLimitSoc(soc: number): void {
|
||||
api.post(`${this.apiVehicle}limitsoc/${soc}`);
|
||||
},
|
||||
updateStaticPlan(plan: StaticPlan): void {
|
||||
const timeISO = plan.time.toISOString();
|
||||
if (this.loadpoint?.socBasedPlanning) {
|
||||
const p = plan as StaticSocPlan;
|
||||
api.post(`${this.apiVehicle}plan/soc/${p.soc}/${timeISO}`, null);
|
||||
} else {
|
||||
const p = plan as StaticEnergyPlan;
|
||||
api.post(`${this.apiLoadpoint}plan/energy/${p.energy}/${timeISO}`, null);
|
||||
}
|
||||
},
|
||||
removeStaticPlan(): void {
|
||||
if (this.loadpoint?.socBasedPlanning) {
|
||||
api.delete(`${this.apiVehicle}plan/soc`);
|
||||
} else {
|
||||
api.delete(`${this.apiLoadpoint}plan/energy`);
|
||||
}
|
||||
},
|
||||
updateRepeatingPlans(plans: RepeatingPlan[]): void {
|
||||
api.post(`${this.apiVehicle}plan/repeating`, plans);
|
||||
},
|
||||
updatePlanStrategy(strategy: PlanStrategy): void {
|
||||
if (this.loadpoint?.socBasedPlanning) {
|
||||
api.post(`${this.apiVehicle}plan/strategy`, strategy);
|
||||
} else {
|
||||
api.post(`${this.apiLoadpoint}plan/strategy`, strategy);
|
||||
}
|
||||
},
|
||||
showDepartureTab(): void {
|
||||
this.activeTab = "departure";
|
||||
},
|
||||
showArrivalTab(): void {
|
||||
this.activeTab = "arrival";
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -31,14 +31,6 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LoadpointSettingsModal
|
||||
:id="id"
|
||||
v-bind="settingsModal"
|
||||
@maxcurrent-updated="setMaxCurrent"
|
||||
@mincurrent-updated="setMinCurrent"
|
||||
@phasesconfigured-updated="setPhasesConfigured"
|
||||
@batteryboostlimit-updated="setBatteryBoostLimit"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="remoteDisabled"
|
||||
|
|
@ -91,12 +83,14 @@
|
|||
<Vehicle
|
||||
class="flex-grow-1 d-flex flex-column justify-content-end"
|
||||
v-bind="vehicleProps"
|
||||
:soc-per-kwh="socPerKwh"
|
||||
@limit-soc-updated="setLimitSoc"
|
||||
@limit-energy-updated="setLimitEnergy"
|
||||
@change-vehicle="changeVehicle"
|
||||
@remove-vehicle="removeVehicle"
|
||||
@open-loadpoint-settings="openSettingsModal"
|
||||
@batteryboost-updated="setBatteryBoost"
|
||||
@open-modal="(openArrivalTab) => $emit('open-charging-plan-modal', openArrivalTab)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -115,11 +109,9 @@ import SettingsButton from "./SettingsButton.vue";
|
|||
import SettingsModal from "./SettingsModal.vue";
|
||||
import VehicleIcon from "../VehicleIcon";
|
||||
import SessionInfo from "./SessionInfo.vue";
|
||||
import Modal from "bootstrap/js/dist/modal";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type {
|
||||
CHARGE_MODE,
|
||||
PHASES,
|
||||
PHASE_ACTION,
|
||||
PV_ACTION,
|
||||
CHARGER_STATUS_REASON,
|
||||
|
|
@ -139,7 +131,6 @@ export default defineComponent({
|
|||
Phases,
|
||||
LabelAndValue,
|
||||
LoadpointSettingsButton: SettingsButton,
|
||||
LoadpointSettingsModal: SettingsModal,
|
||||
LoadpointSessionInfo: SessionInfo,
|
||||
VehicleIcon,
|
||||
},
|
||||
|
|
@ -243,7 +234,17 @@ export default defineComponent({
|
|||
forecast: Object as PropType<Forecast>,
|
||||
lastSmartCostLimit: Number,
|
||||
lastSmartFeedInPriorityLimit: Number,
|
||||
vehicleKnown: Boolean,
|
||||
vehicleHasSoc: Boolean,
|
||||
vehicleNotReachable: Boolean,
|
||||
socBasedCharging: Boolean,
|
||||
socBasedPlanning: Boolean,
|
||||
capacity: Number,
|
||||
range: Number,
|
||||
rangePerSoc: Number,
|
||||
socPerKwh: { type: Number, required: true },
|
||||
},
|
||||
emits: ["open-charging-plan-modal", "open-settings-modal"],
|
||||
data() {
|
||||
return {
|
||||
tickerHandler: null as Timeout,
|
||||
|
|
@ -290,27 +291,10 @@ export default defineComponent({
|
|||
showChargingIndicator() {
|
||||
return this.charging && this.chargePower > 0;
|
||||
},
|
||||
vehicleKnown() {
|
||||
return !!this.vehicleName;
|
||||
},
|
||||
vehicleHasSoc() {
|
||||
return this.vehicleKnown && !this.vehicle?.features?.includes("Offline");
|
||||
},
|
||||
vehicleNotReachable() {
|
||||
// online vehicle that was not reachable at startup
|
||||
const features = this.vehicle?.features || [];
|
||||
return features.includes("Offline") && features.includes("Retryable");
|
||||
},
|
||||
planTimeUnreachable() {
|
||||
// 1 minute tolerance
|
||||
return this.planOverrun > 60;
|
||||
},
|
||||
socBasedCharging() {
|
||||
return this.vehicleHasSoc || this.vehicleSoc > 0;
|
||||
},
|
||||
socBasedPlanning() {
|
||||
return this.socBasedCharging && this.vehicle?.capacity && this.vehicle?.capacity > 0;
|
||||
},
|
||||
pvPossible() {
|
||||
return this.pvConfigured || this.gridConfigured;
|
||||
},
|
||||
|
|
@ -379,15 +363,6 @@ export default defineComponent({
|
|||
setLimitEnergy(kWh: number) {
|
||||
api.post(this.apiPath("limitenergy") + "/" + kWh);
|
||||
},
|
||||
setMaxCurrent(maxCurrent: number) {
|
||||
api.post(this.apiPath("maxcurrent") + "/" + maxCurrent);
|
||||
},
|
||||
setMinCurrent(minCurrent: number) {
|
||||
api.post(this.apiPath("mincurrent") + "/" + minCurrent);
|
||||
},
|
||||
setPhasesConfigured(phases: PHASES) {
|
||||
api.post(this.apiPath("phases") + "/" + phases);
|
||||
},
|
||||
changeVehicle(name: string) {
|
||||
api.post(this.apiPath("vehicle") + `/${name}`);
|
||||
},
|
||||
|
|
@ -397,9 +372,6 @@ export default defineComponent({
|
|||
setBatteryBoost(batteryBoost: boolean) {
|
||||
api.post(this.apiPath("batteryboost") + `/${batteryBoost ? "1" : "0"}`);
|
||||
},
|
||||
setBatteryBoostLimit(limit: number) {
|
||||
api.post(this.apiPath("batteryboostlimit") + "/" + limit);
|
||||
},
|
||||
fmtPower(value: number) {
|
||||
return this.fmtW(value, POWER_UNIT.AUTO);
|
||||
},
|
||||
|
|
@ -407,10 +379,7 @@ export default defineComponent({
|
|||
return this.fmtWh(value, POWER_UNIT.AUTO);
|
||||
},
|
||||
openSettingsModal() {
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById(`loadpointSettingsModal_${this.id}`) as HTMLElement
|
||||
);
|
||||
modal.show();
|
||||
this.$emit("open-settings-modal");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Loadpoints from "./Loadpoints.vue";
|
||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||
import { SMART_COST_TYPE } from "@/types/evcc";
|
||||
import { CURRENCY, SMART_COST_TYPE } from "@/types/evcc";
|
||||
|
||||
// Create LoadpointCompact structure for the Loadpoints component
|
||||
const createLoadpoint = (opts: any = {}) => {
|
||||
|
|
@ -84,7 +84,7 @@ const baseArgs = {
|
|||
tariffGrid: 0.144,
|
||||
tariffCo2: 252,
|
||||
tariffFeedIn: 0.08,
|
||||
currency: "EUR",
|
||||
currency: CURRENCY.EUR,
|
||||
gridConfigured: true,
|
||||
pvConfigured: true,
|
||||
batteryConfigured: true,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
class="h-100"
|
||||
:class="{ 'loadpoint-unselected': !selected(loadpoint.id) }"
|
||||
@click="goTo(loadpoint.id)"
|
||||
@open-charging-plan-modal="
|
||||
(openArrivalTab) => openChargingPlanModal(loadpoint.id, openArrivalTab)
|
||||
"
|
||||
@open-settings-modal="openSettingsModal(loadpoint.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -58,6 +62,28 @@
|
|||
<shopicon-bold-circle v-else class="indicator-icon"></shopicon-bold-circle>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<ChargingPlanModal
|
||||
ref="chargingPlanModal"
|
||||
:loadpoints="loadpoints"
|
||||
:vehicles="vehicles"
|
||||
:smartCostType="smartCostType"
|
||||
:currency="currency"
|
||||
:forecast="forecast"
|
||||
/>
|
||||
<SettingsModal
|
||||
ref="settingsModal"
|
||||
:loadpoints="loadpoints"
|
||||
:multipleLoadpoints="multipleLoadpoints"
|
||||
:currency="currency"
|
||||
:tariffGrid="tariffGrid"
|
||||
:smartFeedInPriorityAvailable="smartFeedInPriorityAvailable"
|
||||
:smartCostAvailable="smartCostAvailable"
|
||||
:smartCostType="smartCostType"
|
||||
:battery-configured="batteryConfigured"
|
||||
:forecast="forecast"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -68,11 +94,21 @@ import "@h2d2/shopicons/es/filled/lightning";
|
|||
|
||||
import Loadpoint from "./Loadpoint.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { UiLoadpoint, SMART_COST_TYPE, Timeout, Vehicle, BATTERY_MODE } from "@/types/evcc";
|
||||
import type {
|
||||
UiLoadpoint,
|
||||
SMART_COST_TYPE,
|
||||
Timeout,
|
||||
Vehicle,
|
||||
BATTERY_MODE,
|
||||
Forecast,
|
||||
CURRENCY,
|
||||
} from "@/types/evcc";
|
||||
import ChargingPlanModal from "../ChargingPlans/ChargingPlanModal.vue";
|
||||
import SettingsModal from "../Loadpoints/SettingsModal.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Loadpoints",
|
||||
components: { Loadpoint },
|
||||
components: { Loadpoint, ChargingPlanModal, SettingsModal },
|
||||
props: {
|
||||
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||
vehicles: { type: Array as PropType<Vehicle[]> },
|
||||
|
|
@ -82,14 +118,14 @@ export default defineComponent({
|
|||
tariffGrid: Number,
|
||||
tariffCo2: Number,
|
||||
tariffFeedIn: Number,
|
||||
currency: String,
|
||||
currency: String as PropType<CURRENCY>,
|
||||
selectedId: String,
|
||||
gridConfigured: Boolean,
|
||||
pvConfigured: Boolean,
|
||||
batteryConfigured: Boolean,
|
||||
batterySoc: Number,
|
||||
batteryMode: String as PropType<BATTERY_MODE>,
|
||||
forecast: Object, // as PropType<Forecast>,
|
||||
forecast: Object as PropType<Forecast>,
|
||||
},
|
||||
emits: ["id-changed"],
|
||||
data() {
|
||||
|
|
@ -188,6 +224,25 @@ export default defineComponent({
|
|||
}
|
||||
}, 1000);
|
||||
},
|
||||
openChargingPlanModal(loadpointId: string, openArrivalTab = false) {
|
||||
const modal = this.$refs["chargingPlanModal"] as
|
||||
| InstanceType<typeof ChargingPlanModal>
|
||||
| undefined;
|
||||
|
||||
if (openArrivalTab) {
|
||||
modal?.showArrivalTab();
|
||||
} else {
|
||||
modal?.showDepartureTab();
|
||||
}
|
||||
|
||||
modal?.open(loadpointId);
|
||||
},
|
||||
openSettingsModal(loadpointId: string) {
|
||||
const modal = this.$refs["settingsModal"] as
|
||||
| InstanceType<typeof SettingsModal>
|
||||
| undefined;
|
||||
modal?.open(loadpointId);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,31 @@
|
|||
<template>
|
||||
<GenericModal
|
||||
:id="`loadpointSettingsModal_${id}`"
|
||||
:title="$t('main.loadpointSettings.title', [title])"
|
||||
ref="modal"
|
||||
:title="$t('main.loadpointSettings.title', [loadpoint?.title])"
|
||||
size="xl"
|
||||
data-testid="loadpoint-settings-modal"
|
||||
@opened="modalVisible"
|
||||
@open="modalVisible"
|
||||
@closed="modalInvisible"
|
||||
>
|
||||
<div class="container">
|
||||
<SmartCostLimit
|
||||
:current-limit="smartCostLimit"
|
||||
:last-limit="lastSmartCostLimit"
|
||||
:current-limit="loadpoint?.smartCostLimit || null"
|
||||
:last-limit="loadpoint?.lastSmartCostLimit"
|
||||
:smart-cost-type="smartCostType"
|
||||
:currency="currency"
|
||||
is-loadpoint
|
||||
:loadpoint-id="loadpointId"
|
||||
:loadpoint-id="id"
|
||||
:multiple-loadpoints="multipleLoadpoints"
|
||||
:possible="smartCostAvailable"
|
||||
:tariff="forecast?.planner"
|
||||
class="mt-2 mb-4"
|
||||
/>
|
||||
<SmartFeedInPriority
|
||||
:current-limit="smartFeedInPriorityLimit"
|
||||
:last-limit="lastSmartFeedInPriorityLimit"
|
||||
:current-limit="loadpoint?.smartFeedInPriorityLimit || null"
|
||||
:last-limit="loadpoint?.lastSmartFeedInPriorityLimit"
|
||||
:currency="currency"
|
||||
:loadpoint-id="loadpointId"
|
||||
:loadpoint-id="id"
|
||||
:multiple-loadpoints="multipleLoadpoints"
|
||||
:possible="smartFeedInPriorityAvailable"
|
||||
:tariff="forecast?.feedin"
|
||||
|
|
@ -34,7 +35,7 @@
|
|||
v-if="batteryBoostAvailable"
|
||||
v-bind="batteryBoostProps"
|
||||
class="mt-2"
|
||||
@batteryboostlimit-updated="changeBatteryBoostLimit"
|
||||
@batteryboostlimit-updated="setBatteryBoostLimit"
|
||||
/>
|
||||
<h6>
|
||||
{{ $t("main.loadpointSettings.currents") }}
|
||||
|
|
@ -47,7 +48,7 @@
|
|||
{{ $t("main.loadpointSettings.phasesConfigured.label") }}
|
||||
</label>
|
||||
<div class="col-sm-8 pe-0">
|
||||
<p v-if="!chargerPhases1p3p" class="mt-0 mb-2">
|
||||
<p v-if="!loadpoint?.chargerPhases1p3p" class="mt-0 mb-2">
|
||||
<small>
|
||||
{{ $t("main.loadpointSettings.phasesConfigured.no1p3pSupport") }}</small
|
||||
>
|
||||
|
|
@ -60,7 +61,7 @@
|
|||
type="radio"
|
||||
:name="formId('phases')"
|
||||
:value="phases"
|
||||
@change="changePhasesConfigured"
|
||||
@change="setPhasesConfigured"
|
||||
/>
|
||||
<label class="form-check-label" :for="formId(`phases_${phases}`)">
|
||||
{{ $t(`main.loadpointSettings.phasesConfigured.phases_${phases}`) }}
|
||||
|
|
@ -89,7 +90,7 @@
|
|||
:id="formId('maxcurrent')"
|
||||
v-model.number="selectedMaxCurrent"
|
||||
class="form-select form-select-sm"
|
||||
@change="changeMaxCurrent"
|
||||
@change="setMaxCurrent"
|
||||
>
|
||||
<option
|
||||
v-for="{ value, name } in maxCurrentOptions"
|
||||
|
|
@ -111,7 +112,7 @@
|
|||
:id="formId('mincurrent')"
|
||||
v-model.number="selectedMinCurrent"
|
||||
class="form-select form-select-sm"
|
||||
@change="changeMinCurrent"
|
||||
@change="setMinCurrent"
|
||||
>
|
||||
<option
|
||||
v-for="{ value, name } in minCurrentOptions"
|
||||
|
|
@ -135,7 +136,8 @@ import SmartCostLimit from "../Tariff/SmartCostLimit.vue";
|
|||
import SmartFeedInPriority from "../Tariff/SmartFeedInPriority.vue";
|
||||
import SettingsBatteryBoost from "./SettingsBatteryBoost.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { PHASES, CURRENCY, SMART_COST_TYPE, type Forecast } from "@/types/evcc";
|
||||
import { PHASES, CURRENCY, SMART_COST_TYPE, type Forecast, type UiLoadpoint } from "@/types/evcc";
|
||||
import api from "@/api";
|
||||
|
||||
const V = 230;
|
||||
|
||||
|
|
@ -163,49 +165,46 @@ export default defineComponent({
|
|||
},
|
||||
mixins: [formatter, collector],
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
phasesConfigured: { type: Number, default: 0 },
|
||||
chargerPhases1p3p: Boolean,
|
||||
chargerSinglePhase: Boolean,
|
||||
batteryBoostLimit: { type: Number, default: 100 },
|
||||
batteryBoostAvailable: Boolean,
|
||||
mode: String,
|
||||
minSoc: Number,
|
||||
maxCurrent: { type: Number, default: 0 },
|
||||
minCurrent: { type: Number, default: 0 },
|
||||
title: String,
|
||||
smartCostLimit: { type: Number as PropType<number | null>, default: null },
|
||||
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||
batteryConfigured: Boolean,
|
||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||
smartCostAvailable: Boolean,
|
||||
smartFeedInPriorityLimit: { type: Number as PropType<number | null>, default: null },
|
||||
smartFeedInPriorityAvailable: Boolean,
|
||||
tariffGrid: Number,
|
||||
currency: String as PropType<CURRENCY>,
|
||||
multipleLoadpoints: Boolean,
|
||||
forecast: Object as PropType<Forecast>,
|
||||
lastSmartCostLimit: Number,
|
||||
lastSmartFeedInPriorityLimit: Number,
|
||||
},
|
||||
emits: [
|
||||
"phasesconfigured-updated",
|
||||
"maxcurrent-updated",
|
||||
"mincurrent-updated",
|
||||
"batteryboostlimit-updated",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
selectedMaxCurrent: this.maxCurrent,
|
||||
selectedMinCurrent: this.minCurrent,
|
||||
selectedPhases: this.phasesConfigured,
|
||||
id: undefined as string | undefined,
|
||||
selectedMaxCurrent: undefined as number | undefined,
|
||||
selectedMinCurrent: undefined as number | undefined,
|
||||
selectedPhases: undefined as number | undefined,
|
||||
isModalVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
loadpoint() {
|
||||
return this.loadpoints.find((loadpoint) => loadpoint.id === this.id);
|
||||
},
|
||||
maxCurrent() {
|
||||
return this.loadpoint?.maxCurrent;
|
||||
},
|
||||
minCurrent() {
|
||||
return this.loadpoint?.minCurrent;
|
||||
},
|
||||
batteryBoostLimit() {
|
||||
return this.loadpoint?.batteryBoostLimit;
|
||||
},
|
||||
phasesConfigured() {
|
||||
return this.loadpoint?.phasesConfigured;
|
||||
},
|
||||
phasesOptions() {
|
||||
if (this.chargerSinglePhase) {
|
||||
if (this.loadpoint?.chargerSinglePhase) {
|
||||
return [];
|
||||
}
|
||||
if (this.chargerPhases1p3p) {
|
||||
if (this.loadpoint?.chargerPhases1p3p) {
|
||||
// automatic switching
|
||||
return [AUTO, THREE_PHASES, ONE_PHASE];
|
||||
}
|
||||
|
|
@ -215,33 +214,32 @@ export default defineComponent({
|
|||
batteryBoostProps() {
|
||||
return this.collectProps(SettingsBatteryBoost);
|
||||
},
|
||||
maxPhases(): number {
|
||||
if (this.chargerPhases1p3p && this.phasesConfigured === AUTO) {
|
||||
maxPhases() {
|
||||
if (this.loadpoint?.chargerPhases1p3p && this.phasesConfigured === AUTO) {
|
||||
return THREE_PHASES;
|
||||
}
|
||||
return this.phasesConfigured;
|
||||
},
|
||||
minPhases(): number {
|
||||
if (this.chargerPhases1p3p && this.phasesConfigured === AUTO) {
|
||||
minPhases() {
|
||||
if (this.loadpoint?.chargerPhases1p3p && this.phasesConfigured === AUTO) {
|
||||
return ONE_PHASE;
|
||||
}
|
||||
return this.phasesConfigured;
|
||||
},
|
||||
minCurrentOptions() {
|
||||
const opt1 = [...range(Math.floor(this.maxCurrent), 1), 0.5, 0.25, 0.125];
|
||||
const opt1 = [...range(Math.floor(this.maxCurrent ?? 0), 1), 0.5, 0.25, 0.125];
|
||||
// ensure that current value is always included
|
||||
const opt2 = insertSorted(opt1, this.minCurrent);
|
||||
const opt2 = insertSorted(opt1, this.minCurrent ?? 0);
|
||||
return opt2.map((value) => this.currentOption(value, value === 6, this.minPhases));
|
||||
},
|
||||
maxCurrentOptions() {
|
||||
const opt1 = range(MAX_CURRENT, Math.ceil(this.minCurrent));
|
||||
const opt1 = range(MAX_CURRENT, Math.ceil(this.minCurrent ?? 0));
|
||||
// ensure that current value is always included
|
||||
const opt2 = insertSorted(opt1, this.maxCurrent);
|
||||
const opt2 = insertSorted(opt1, this.maxCurrent ?? 0);
|
||||
return opt2.map((value) => this.currentOption(value, value === 16, this.maxPhases));
|
||||
},
|
||||
|
||||
loadpointId() {
|
||||
return this.id;
|
||||
batteryBoostAvailable() {
|
||||
return this.batteryConfigured;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -256,22 +254,36 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
fmtPhasePower(current: number, phases: PHASES) {
|
||||
return this.fmtW(V * current * phases);
|
||||
open(loadpointId: string) {
|
||||
this.id = loadpointId;
|
||||
this.selectedPhases = this.phasesConfigured;
|
||||
this.selectedMaxCurrent = this.maxCurrent;
|
||||
this.selectedMinCurrent = this.minCurrent;
|
||||
const modalRef = this.$refs["modal"] as InstanceType<typeof GenericModal> | undefined;
|
||||
modalRef?.open();
|
||||
},
|
||||
apiPath(func: string) {
|
||||
return "loadpoints/" + this.id + "/" + func;
|
||||
},
|
||||
fmtPhasePower(current?: number, phases?: PHASES) {
|
||||
return this.fmtW(V * (current || 0) * (phases || 0));
|
||||
},
|
||||
formId(name: string) {
|
||||
return `loadpoint_${this.id}_${name}`;
|
||||
},
|
||||
changeMaxCurrent() {
|
||||
this.$emit("maxcurrent-updated", this.selectedMaxCurrent);
|
||||
setMaxCurrent() {
|
||||
api.post(this.apiPath("maxcurrent") + "/" + this.selectedMaxCurrent);
|
||||
},
|
||||
changeMinCurrent() {
|
||||
this.$emit("mincurrent-updated", this.selectedMinCurrent);
|
||||
setMinCurrent() {
|
||||
api.post(this.apiPath("mincurrent") + "/" + this.selectedMinCurrent);
|
||||
},
|
||||
changePhasesConfigured() {
|
||||
this.$emit("phasesconfigured-updated", this.selectedPhases);
|
||||
setPhasesConfigured() {
|
||||
api.post(this.apiPath("phases") + "/" + this.selectedPhases);
|
||||
},
|
||||
currentOption(current: number, isDefault: boolean, phases: number) {
|
||||
setBatteryBoostLimit(limit: number) {
|
||||
api.post(this.apiPath("batteryboostlimit") + "/" + limit);
|
||||
},
|
||||
currentOption(current: number, isDefault: boolean, phases?: number) {
|
||||
const kw = this.fmtPhasePower(current, phases);
|
||||
let name = `${this.fmtNumber(current, undefined)} A (${kw})`;
|
||||
if (isDefault) {
|
||||
|
|
@ -285,9 +297,6 @@ export default defineComponent({
|
|||
modalInvisible() {
|
||||
this.isModalVisible = false;
|
||||
},
|
||||
changeBatteryBoostLimit(limit: number) {
|
||||
this.$emit("batteryboostlimit-updated", limit);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default defineComponent({
|
|||
},
|
||||
lastLimit: Number,
|
||||
currency: String as PropType<CURRENCY>,
|
||||
loadpointId: { type: String, required: true },
|
||||
loadpointId: String,
|
||||
multipleLoadpoints: Boolean,
|
||||
possible: Boolean,
|
||||
tariff: Array,
|
||||
|
|
@ -75,7 +75,9 @@ export default defineComponent({
|
|||
await api.post(`${url}/${encodeURIComponent(limit)}`);
|
||||
},
|
||||
saveLastLimit(limit: number) {
|
||||
setLoadpointLastSmartFeedInPriorityLimit(this.loadpointId, limit);
|
||||
if (this.loadpointId) {
|
||||
setLoadpointLastSmartFeedInPriorityLimit(this.loadpointId, limit);
|
||||
}
|
||||
},
|
||||
async deleteLimit() {
|
||||
// save last selected value to be suggest again when reactivating limit
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
v-bind="vehicleStatus"
|
||||
class="mb-2"
|
||||
@open-loadpoint-settings="$emit('open-loadpoint-settings')"
|
||||
@open-minsoc-settings="openMinSocSettings"
|
||||
@open-minsoc-settings="openPlanModal(true)"
|
||||
@open-plan-modal="openPlanModal"
|
||||
/>
|
||||
<div class="mt-2 mb-4 d-flex gap-2">
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
class="flex-grow-1 target-charge"
|
||||
v-bind="chargingPlan"
|
||||
:disabled="chargingPlanDisabled"
|
||||
@open-modal="$emit('open-modal')"
|
||||
/>
|
||||
<LimitSocSelect
|
||||
v-if="socBasedCharging"
|
||||
|
|
@ -86,7 +87,7 @@ import Status from "./Status.vue";
|
|||
import ChargingPlan from "../ChargingPlans/ChargingPlan.vue";
|
||||
import LimitSocSelect from "./LimitSocSelect.vue";
|
||||
import LimitEnergySelect from "./LimitEnergySelect.vue";
|
||||
import { distanceUnit, distanceValue } from "@/units.ts";
|
||||
import { distanceUnit } from "@/units.ts";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import {
|
||||
CHARGE_MODE,
|
||||
|
|
@ -97,6 +98,7 @@ import {
|
|||
} from "@/types/evcc";
|
||||
import type { PlanStrategy } from "@/components/ChargingPlans/types";
|
||||
import BatteryBoostButton from "../Loadpoints/BatteryBoostButton.vue";
|
||||
import type ChargingPlanModal from "../ChargingPlans/ChargingPlanModal.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Vehicle",
|
||||
|
|
@ -169,6 +171,10 @@ export default defineComponent({
|
|||
vehicleLimitSoc: Number,
|
||||
vehicleNotReachable: Boolean,
|
||||
minSocNotReached: Boolean,
|
||||
capacity: Number,
|
||||
range: Number,
|
||||
rangePerSoc: Number,
|
||||
socPerKwh: { type: Number, required: true },
|
||||
},
|
||||
emits: [
|
||||
"limit-soc-updated",
|
||||
|
|
@ -177,20 +183,21 @@ export default defineComponent({
|
|||
"remove-vehicle",
|
||||
"open-loadpoint-settings",
|
||||
"batteryboost-updated",
|
||||
"open-modal",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
displayLimitSoc: this.effectiveLimitSoc,
|
||||
statusOverride: undefined as VehicleStatus | undefined,
|
||||
chargingPlanModal: this.$refs["chargingPlanModal"] as
|
||||
| InstanceType<typeof ChargingPlanModal>
|
||||
| undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.vehicle?.title || "";
|
||||
},
|
||||
capacity() {
|
||||
return this.vehicle?.capacity || 0;
|
||||
},
|
||||
icon() {
|
||||
return this.vehicle?.icon || "";
|
||||
},
|
||||
|
|
@ -230,24 +237,9 @@ export default defineComponent({
|
|||
}
|
||||
return this.$t("main.vehicle.vehicleSoc");
|
||||
},
|
||||
range() {
|
||||
return distanceValue(this.vehicleRange);
|
||||
},
|
||||
rangeUnit() {
|
||||
return distanceUnit();
|
||||
},
|
||||
rangePerSoc() {
|
||||
if (this.vehicleSoc > 10 && this.range) {
|
||||
return Math.round((this.range / this.vehicleSoc) * 1e2) / 1e2;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
socPerKwh() {
|
||||
if (this.capacity > 0) {
|
||||
return 100 / this.capacity;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
chargedSoc() {
|
||||
const value = this.socPerKwh * (this.chargedEnergy / 1e3);
|
||||
return value > 1 ? `+${this.fmtPercentage(value)}` : null;
|
||||
|
|
@ -287,19 +279,12 @@ export default defineComponent({
|
|||
fmtEnergy(value: number) {
|
||||
return this.fmtWh(value, value == 0 ? POWER_UNIT.KW : POWER_UNIT.AUTO);
|
||||
},
|
||||
openPlanModal() {
|
||||
(
|
||||
this.$refs["chargingPlan"] as InstanceType<typeof ChargingPlan> | undefined
|
||||
)?.openPlanModal();
|
||||
openPlanModal(openArrivalTab = false) {
|
||||
this.$emit("open-modal", openArrivalTab);
|
||||
},
|
||||
handleBoostStatus(status: VehicleStatus) {
|
||||
this.statusOverride = status;
|
||||
},
|
||||
openMinSocSettings() {
|
||||
(
|
||||
this.$refs["chargingPlan"] as InstanceType<typeof ChargingPlan> | undefined
|
||||
)?.openPlanModal(true);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ export interface Loadpoint {
|
|||
vehicleSoc: number;
|
||||
vehicleTitle: string;
|
||||
vehicleWelcomeActive: boolean;
|
||||
batteryBoostLimit: number;
|
||||
}
|
||||
|
||||
export interface UiLoadpoint extends Loadpoint {
|
||||
|
|
@ -336,6 +337,18 @@ export interface UiLoadpoint extends Loadpoint {
|
|||
visible: boolean;
|
||||
lastSmartCostLimit: number | undefined;
|
||||
lastSmartFeedInPriorityLimit: number | undefined;
|
||||
range: number;
|
||||
vehicleRange: number;
|
||||
vehicleSoc: number;
|
||||
capacity: number;
|
||||
vehicleKnown: boolean;
|
||||
vehicleHasSoc: boolean;
|
||||
socBasedCharging: boolean;
|
||||
socBasedPlanning: boolean;
|
||||
sessionInfo: SessionInfoKey | undefined;
|
||||
rangePerSoc: number | undefined;
|
||||
socPerKwh: number;
|
||||
vehicleNotReachable: boolean;
|
||||
}
|
||||
|
||||
export enum THEME {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import settings from "./settings";
|
||||
import type { UiLoadpoint, SessionInfoKey, Loadpoint, Vehicle } from "./types/evcc";
|
||||
import { distanceValue } from "./units";
|
||||
|
||||
const get = (id: string) => {
|
||||
if (!settings.loadpoints[id]) {
|
||||
|
|
@ -15,12 +16,29 @@ export const convertToUiLoadpoints = (
|
|||
if (loadpoints.length === 0) return [];
|
||||
|
||||
const mappedLoadpoints = loadpoints.map((lp, originalIndex) => {
|
||||
const vehicle = vehicles[lp.vehicleName];
|
||||
const vehicle = vehicles[lp.vehicleName] as Vehicle | undefined;
|
||||
const id = `${originalIndex + 1}`;
|
||||
|
||||
const vehicleRange = lp.vehicleRange;
|
||||
const vehicleSoc = lp.vehicleSoc;
|
||||
const capacity = vehicle?.capacity || 0;
|
||||
const range = distanceValue(vehicleRange);
|
||||
const vehicleKnown = !!lp.vehicleName;
|
||||
const vehicleHasSoc = vehicleKnown && !vehicle?.features?.includes("Offline");
|
||||
const socBasedCharging = vehicleHasSoc || lp.vehicleSoc > 0;
|
||||
const socBasedPlanning = !!(socBasedCharging && capacity > 0);
|
||||
|
||||
return {
|
||||
...lp,
|
||||
id,
|
||||
range,
|
||||
vehicleRange,
|
||||
vehicleSoc,
|
||||
capacity,
|
||||
vehicleKnown,
|
||||
vehicleHasSoc,
|
||||
socBasedCharging,
|
||||
socBasedPlanning,
|
||||
displayTitle: vehicle?.title || lp.title || "Charging point",
|
||||
icon: lp.chargerIcon || vehicle?.icon || "car",
|
||||
order: getLoadpointOrder(id),
|
||||
|
|
@ -28,7 +46,13 @@ export const convertToUiLoadpoints = (
|
|||
sessionInfo: getLoadpointSessionInfo(id),
|
||||
lastSmartCostLimit: getLoadpointLastSmartCostLimit(id),
|
||||
lastSmartFeedInPriorityLimit: getLoadpointLastSmartFeedInPriorityLimit(id),
|
||||
};
|
||||
rangePerSoc:
|
||||
vehicleSoc > 10 && range ? Math.round((range / vehicleSoc) * 1e2) / 1e2 : undefined,
|
||||
socPerKwh: capacity > 0 ? 100 / capacity : 0,
|
||||
vehicleNotReachable:
|
||||
(vehicle?.features || []).includes("Offline") &&
|
||||
(vehicle?.features || []).includes("Retryable"),
|
||||
} satisfies UiLoadpoint;
|
||||
});
|
||||
|
||||
// Sort by order (loadpoints with no order go to the end)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ test.describe("boost", async () => {
|
|||
await lp.getByTestId("mode").getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("20 %");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Battery Boost").selectOption("20 %");
|
||||
await expect(modal.getByTestId("battery-boost")).toContainText(
|
||||
"Allow fast charging from home battery until it's drained to 20%."
|
||||
);
|
||||
|
|
@ -49,7 +50,8 @@ test.describe("boost", async () => {
|
|||
await lp.getByTestId("mode").getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("90 %");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Battery Boost").selectOption("90 %");
|
||||
await expect(modal.getByTestId("battery-boost")).toContainText("drained to 90%");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await modal.getByLabel("Close").click();
|
||||
|
|
@ -67,7 +69,8 @@ test.describe("boost", async () => {
|
|||
await lp.getByTestId("mode").getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("20 %");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Battery Boost").selectOption("20 %");
|
||||
await expect(modal.getByTestId("battery-boost")).toContainText("drained to 20%");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await modal.getByLabel("Close").click();
|
||||
|
|
@ -87,7 +90,7 @@ test.describe("boost", async () => {
|
|||
await lp1.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("0 %");
|
||||
await modal.getByLabel("Battery Boost").selectOption("0 %");
|
||||
await modal.getByLabel("Close").click();
|
||||
await expectModalHidden(modal);
|
||||
|
||||
|
|
@ -135,7 +138,8 @@ test.describe("boost", async () => {
|
|||
// visible after setting limit
|
||||
await lp.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").last();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("50 %");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Battery Boost").selectOption("50 %");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await modal.getByLabel("Close").click();
|
||||
await expectModalHidden(modal);
|
||||
|
|
|
|||
|
|
@ -207,9 +207,10 @@ test.describe("ext meter order", async () => {
|
|||
|
||||
// Verify order in main UI consumer dropdown
|
||||
await page.goto("/#/");
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
|
||||
await page.getByTestId("energyflow").click();
|
||||
await page.getByRole("button", { name: "Consumption" }).click();
|
||||
const consumers = await page.getByTestId("energyflow-entry-consumer");
|
||||
const consumers = page.getByTestId("energyflow-entry-consumer");
|
||||
await expect(consumers.nth(0)).toContainText("Meter 1");
|
||||
await expect(consumers.nth(1)).toContainText("Meter 2");
|
||||
await expect(consumers.nth(2)).toContainText("Meter 3");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { expectModalHidden, openMoreMenu } from "./utils";
|
||||
import { expectModalHidden, expectModalVisible, openMoreMenu } from "./utils";
|
||||
import { ChildProcess } from "child_process";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
|
@ -38,7 +38,7 @@ test.describe("demo mode", async () => {
|
|||
await openMoreMenu(page);
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
const loginModal = page.getByTestId("login-modal");
|
||||
await expect(loginModal).toBeVisible();
|
||||
await expectModalVisible(loginModal);
|
||||
await expect(loginModal).toContainText("Login is not supported in demo mode.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { expectModalHidden, expectModalVisible } from "./utils";
|
||||
import {
|
||||
startSimulator,
|
||||
stopSimulator,
|
||||
|
|
@ -39,12 +40,12 @@ test.describe("smart cost limit", async () => {
|
|||
await page.goto("/");
|
||||
await page.getByTestId("loadpoint-settings-button").nth(1).click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal");
|
||||
await expect(modal).toBeVisible();
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Enable limit").check();
|
||||
await modal.getByLabel("Price limit").selectOption("≤ 40.0 ct/kWh");
|
||||
await expect(modal.getByTestId("active-hours")).toHaveText(["Active time", "96 hr"].join(""));
|
||||
await modal.getByLabel("Close").click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expectModalHidden(modal);
|
||||
await expect(page.getByTestId("vehicle-status-charger")).toHaveText("Charging…");
|
||||
await expect(page.getByTestId("vehicle-status-smartcost")).toHaveText(/[24]0\.0 ct ≤ 40\.0 ct/);
|
||||
});
|
||||
|
|
@ -52,12 +53,12 @@ test.describe("smart cost limit", async () => {
|
|||
await page.goto("/");
|
||||
await page.getByTestId("loadpoint-settings-button").nth(1).click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal");
|
||||
await expect(modal).toBeVisible();
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByLabel("Enable limit").check();
|
||||
await modal.getByLabel("Price limit").selectOption("≤ 10.0 ct/kWh");
|
||||
await expect(modal.getByTestId("active-hours")).toHaveText("Active time");
|
||||
await modal.getByLabel("Close").click();
|
||||
await expect(modal).not.toBeVisible();
|
||||
await expectModalHidden(modal);
|
||||
await expect(page.getByTestId("vehicle-status-charger")).toHaveText("Charging…");
|
||||
await expect(page.getByTestId("vehicle-status-smartcost")).toHaveText("≤ 10.0 ct");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue