evcc-io/assets/js/components/Vehicles/Vehicle.vue
andig c3f3384bfc
API: generate /api/state documentation from UI types (#32431)
Co-authored-by: Michael Geers <michael@geers.tv>
2026-08-03 11:06:37 +00:00

308 lines
8 KiB
Vue

<template>
<div class="vehicle pt-4">
<VehicleTitle
v-if="!integratedDevice"
v-bind="vehicleTitleProps"
@change-vehicle="changeVehicle"
@remove-vehicle="removeVehicle"
/>
<VehicleStatus
v-bind="vehicleStatus"
class="mb-2"
@open-loadpoint-settings="$emit('open-loadpoint-settings')"
@open-minsoc-settings="openVehicleSettings"
@open-plan-modal="$emit('open-modal')"
/>
<div class="mt-2 mb-4 d-flex gap-2">
<BatteryBoostButton
v-if="showBoostButton"
class="flex-grow-0"
v-bind="batteryBoostButtonProps"
@updated="$emit('batteryboost-updated', $event)"
@status="handleBoostStatus"
/>
<VehicleSoc
class="flex-grow-1 position-relative"
v-bind="vehicleSocProps"
@limit-soc-updated="limitSocUpdated"
@limit-soc-drag="limitSocDrag"
@plan-clicked="$emit('open-modal')"
/>
</div>
<div class="details d-flex flex-wrap justify-content-between">
<LabelAndValue
v-if="socBasedCharging"
class="flex-grow-1"
:label="vehicleSocTitle"
:value="formattedSoc"
:extraValue="range ? `${fmtNumber(range, 0)} ${rangeUnit}` : ''"
data-testid="current-soc"
align="start"
/>
<LabelAndValue
v-else
class="flex-grow-1"
:label="$t('main.loadpoint.charged')"
:value="fmtEnergy(chargedEnergy)"
:extraValue="chargedSoc || ''"
data-testid="current-energy"
align="start"
/>
<ChargingPlan
v-if="!heating"
ref="chargingPlan"
class="flex-grow-1 target-charge"
v-bind="chargingPlan"
:disabled="chargingPlanDisabled"
@open-modal="$emit('open-modal')"
/>
<LimitSocSelect
v-if="socBasedCharging"
class="flex-grow-1 text-end"
:limit-soc="displayLimitSoc"
:range-per-soc="rangePerSoc"
:heating="heating"
:min-temp="ui?.minTemp ?? 0"
:max-temp="ui?.maxTemp ?? 100"
@limit-soc-updated="limitSocUpdated"
/>
<LimitEnergySelect
v-else
class="flex-grow-1 text-end"
:limit-energy="limitEnergy"
:soc-per-kwh="socPerKwh"
:charged-energy="chargedEnergy"
:capacity="capacity"
@limit-energy-updated="limitEnergyUpdated"
/>
</div>
</div>
</template>
<script lang="ts">
import Modal from "bootstrap/js/dist/modal";
import collector from "@/mixins/collector.ts";
import formatter, { POWER_UNIT } from "@/mixins/formatter";
import LabelAndValue from "../Helper/LabelAndValue.vue";
import Title from "./Title.vue";
import Soc from "./Soc.vue";
import Status from "./Status.vue";
import ChargingPlan from "../ChargingPlans/ChargingPlan.vue";
import LimitSocSelect from "./LimitSocSelect.vue";
import LimitEnergySelect from "./LimitEnergySelect.vue";
import { distanceUnit } from "@/units.ts";
import { defineComponent, type PropType } from "vue";
import {
CHARGE_MODE,
type BATTERY_MODE,
type Forecast,
type VehicleStatus,
type Vehicle,
type LoadpointUi,
type PlanStrategy,
type LoadpointSuggestion,
} from "@/types/evcc";
import BatteryBoostButton from "../Loadpoints/BatteryBoostButton.vue";
import type ChargingPlanModal from "../ChargingPlans/ChargingPlanModal.vue";
export default defineComponent({
name: "Vehicle",
components: {
VehicleTitle: Title,
VehicleSoc: Soc,
VehicleStatus: Status,
LabelAndValue,
ChargingPlan,
LimitSocSelect,
LimitEnergySelect,
BatteryBoostButton,
},
mixins: [collector, formatter],
props: {
chargedEnergy: { type: Number, default: 0 },
charging: Boolean,
vehicleClimaterActive: Boolean,
vehicleWelcomeActive: Boolean,
connected: Boolean,
currency: String,
effectiveLimitSoc: Number,
effectiveMinSoc: { type: Number, default: 0 },
effectivePlanSoc: Number,
effectivePlanTime: String,
effectivePlanStrategy: Object as PropType<PlanStrategy>,
batteryBoost: Boolean,
batteryBoostActive: Boolean,
batteryBoostAvailable: Boolean,
batteryBoostLimit: { type: Number, default: 100 },
batterySoc: Number,
batteryMode: String as PropType<BATTERY_MODE>,
enabled: Boolean,
heating: Boolean,
continuous: Boolean,
id: [String, Number],
integratedDevice: Boolean,
limitEnergy: Number,
mode: String as PropType<CHARGE_MODE>,
chargerStatusReason: String,
phaseAction: String,
phaseRemainingInterpolated: Number,
forecast: Object as PropType<Forecast>,
planActive: Boolean,
planEnergy: Number,
planProjectedStart: String,
planProjectedEnd: String,
planTime: String,
planTimeUnreachable: Boolean,
planOverrun: Number,
pvAction: String,
pvRemainingInterpolated: Number,
sessionSolarPercentage: Number,
smartCostActive: Boolean,
smartCostNextStart: String,
smartCostLimit: Number,
smartCostType: String,
smartFeedInPriorityActive: Boolean,
smartFeedInPriorityNextStart: String,
smartFeedInPriorityLimit: Number,
socBasedCharging: Boolean,
suggestion: Object as PropType<LoadpointSuggestion | null>,
socBasedPlanning: Boolean,
tariffCo2: Number,
tariffGrid: Number,
tariffFeedIn: Number,
vehicle: Object as PropType<Vehicle>,
vehicleDetectionActive: Boolean,
vehicleName: String,
vehicleRange: { type: Number, default: 0 },
vehicles: Array,
vehicleSoc: { type: Number, default: 0 },
vehicleLimitSoc: Number,
vehicleNotReachable: Boolean,
minSocNotReached: Boolean,
ui: Object as PropType<LoadpointUi>,
capacity: Number,
range: Number,
rangePerSoc: Number,
socPerKwh: { type: Number, required: true },
},
emits: [
"limit-soc-updated",
"limit-energy-updated",
"change-vehicle",
"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 || "";
},
icon() {
return this.vehicle?.icon || "";
},
minSoc() {
return this.effectiveMinSoc;
},
vehicleSocProps() {
return this.collectProps(Soc);
},
vehicleStatus() {
return { ...this.collectProps(Status), statusOverride: this.statusOverride };
},
vehicleTitleProps() {
return this.collectProps(Title);
},
chargingPlan() {
return this.collectProps(ChargingPlan);
},
showBoostButton(): boolean {
return this.connected && this.batteryBoostAvailable && this.batteryBoostLimit < 100;
},
batteryBoostButtonProps() {
return this.collectProps(BatteryBoostButton);
},
formattedSoc() {
if (!this.vehicleSoc) {
return "--";
}
if (this.heating) {
return this.fmtTemperature(this.vehicleSoc);
}
return this.fmtPercentage(this.vehicleSoc);
},
vehicleSocTitle() {
if (this.heating) {
return this.$t("main.vehicle.temp");
}
return this.$t("main.vehicle.vehicleSoc");
},
rangeUnit() {
return distanceUnit();
},
chargedSoc() {
const value = this.socPerKwh * (this.chargedEnergy / 1e3);
return value > 1 ? `+${this.fmtPercentage(value)}` : null;
},
chargingPlanDisabled() {
return this.mode && [CHARGE_MODE.OFF, CHARGE_MODE.NOW].includes(this.mode);
},
smartCostDisabled() {
return this.chargingPlanDisabled;
},
smartFeedInPriorityDisabled() {
return this.chargingPlanDisabled;
},
},
watch: {
effectiveLimitSoc() {
this.displayLimitSoc = this.effectiveLimitSoc;
},
},
methods: {
limitSocDrag(limitSoc: number) {
this.displayLimitSoc = limitSoc;
},
limitSocUpdated(limitSoc: number) {
this.displayLimitSoc = limitSoc;
this.$emit("limit-soc-updated", limitSoc);
},
limitEnergyUpdated(limitEnergy: number) {
this.$emit("limit-energy-updated", limitEnergy);
},
changeVehicle(name: string) {
this.$emit("change-vehicle", name);
},
removeVehicle() {
this.$emit("remove-vehicle");
},
fmtEnergy(value: number) {
return this.fmtWh(value, value == 0 ? POWER_UNIT.KW : POWER_UNIT.AUTO);
},
openVehicleSettings() {
Modal.getOrCreateInstance(
document.getElementById("vehicleSettingsModal") as HTMLElement
).show();
},
handleBoostStatus(status: VehicleStatus) {
this.statusOverride = status;
},
},
});
</script>
<style scoped>
.details > div {
flex-grow: 1;
flex-basis: 0;
}
</style>