evcc-io/assets/js/components/Config/LoadpointModal.vue

1114 lines
30 KiB
Vue

<template>
<GenericModal
id="loadpointModal"
ref="modal"
config-modal-name="loadpoint"
:prevent-dismiss="dirty"
:title="modalTitle"
data-testid="loadpoint-modal"
@open="onOpen"
@close="onClose"
@dismiss="onDismiss"
>
<CreateFlowStatus
v-if="autoCreate"
:in-progress="autoCreateInProgress"
:creating-text="$t(`config.loadpoint.creating.${loadpointType}`)"
:success-text="$t('config.loadpoint.created', { name: values.title })"
>
<template #actions>
<a href="#" @click.prevent="autoCreate = false">
{{ $t("config.loadpoint.advancedConfig") }}
</a>
<button type="button" class="btn btn-primary" @click="finish">
{{ $t("config.general.close") }}
</button>
</template>
</CreateFlowStatus>
<div v-else-if="!loadpointType" class="d-flex flex-column gap-4">
<NewDeviceButton
v-for="t in typeChoices"
:key="t"
:title="$t(`config.loadpoint.option.${t}`)"
class="addButton"
@click="selectType(t)"
/>
</div>
<form
v-else
ref="form"
class="container mx-0 px-0"
@submit.prevent="isNew ? create() : update()"
>
<FormRow
id="loadpointParamTitle"
:label="$t('config.loadpoint.titleLabel')"
:example="
loadpointType ? $t(`config.loadpoint.titleExample.${loadpointType}`) : undefined
"
>
<PropertyField
id="loadpointParamTitle"
v-model="values.title"
type="String"
required
/>
</FormRow>
<FormRow
v-if="charger || !isNew"
id="loadpointParamCharger"
:label="$t(`config.loadpoint.chargerLabel.${loadpointType}`)"
:error="!charger ? $t(`config.loadpoint.chargerError.${loadpointType}`) : undefined"
>
<DeviceRefBox
compact
:title="chargerTitle"
:error="!charger || hasDeviceError('charger', values.charger)"
@edit="editCharger"
/>
</FormRow>
<div v-else class="d-flex justify-content-end">
<button
class="btn btn-outline-primary"
type="submit"
:disabled="values.title?.length === 0"
@click.prevent="editCharger"
>
{{ addChargerLabel }}
</button>
</div>
<div v-if="charger || !isNew">
<FormRow
v-if="values.meter"
id="loadpointParamMeter"
class="mb-6"
:label="$t('config.loadpoint.energyMeterLabel')"
:help="$t('config.loadpoint.energyMeterHelp')"
>
<DeviceRefBox
compact
:title="meterTitle"
:error="hasDeviceError('meter', values.meter)"
@edit="editMeter"
/>
</FormRow>
<p v-else>
<button
class="btn btn-link btn-sm text-gray px-0"
style="margin-top: -1rem"
type="button"
tabindex="0"
@click="editMeter"
>
{{ $t(`config.loadpoint.addMeter`) }}
</button>
</p>
</div>
<div v-if="values.charger || !isNew">
<div class="collapsible-wrapper" :class="{ open: !isNew }">
<div class="collapsible-content">
<h6 class="mt-4">{{ $t("config.loadpoint.chargingTitle") }}</h6>
<FormRow
id="loadpointMode"
:label="$t('config.loadpoint.defaultModeLabel')"
:help="
values.defaultMode === ''
? $t(`config.loadpoint.defaultModeHelpKeep`)
: $t(`config.loadpoint.defaultModeHelp.${loadpointType}`)
"
>
<PropertyField
id="loadpointMode"
v-model="values.defaultMode"
type="Choice"
class="w-100"
:choice="defaultModeOptions"
/>
</FormRow>
<FormRow
id="loadpointSolarMode"
:label="$t('config.loadpoint.solarBehaviorLabel')"
:help="
solarMode === 'default'
? $t('config.loadpoint.solarBehaviorDefaultHelp', {
enableDelay: fmtDurationNs(
values.thresholds.enable.delay,
true,
'm'
),
disableDelay: fmtDurationNs(
values.thresholds.disable.delay,
true,
'm'
),
})
: $t('config.loadpoint.solarBehaviorCustomHelp')
"
>
<SelectGroup
id="loadpointSolarMode"
v-model="solarMode"
class="w-100"
:options="[
{
name: $t('config.loadpoint.solarModeMaximum'),
value: 'default',
},
{
name: $t('config.loadpoint.solarModeCustom'),
value: 'custom',
},
]"
transparent
equal-width
/>
</FormRow>
<div v-show="solarMode === 'custom'" class="ms-3 mb-5">
<div class="mb-4">
<div class="d-flex flex-wrap flex-sm-nowrap gap-4">
<FormRow
id="loadpointEnableThreshold"
:label="$t('config.loadpoint.thresholdEnableLabel')"
style="margin-bottom: 0 !important"
>
<PropertyField
id="loadpointEnableThreshold"
v-model="values.thresholds.enable.threshold"
type="Float"
unit="W"
size="w-25 w-min-200"
required
/>
</FormRow>
<FormRow
id="loadpointEnableDelay"
:label="$t('config.loadpoint.thresholdEnableDelayLabel')"
style="margin-bottom: 0 !important"
>
<PropertyField
id="loadpointEnableDelay"
v-model="values.thresholds.enable.delay"
type="Duration"
legacy-duration
unit="minute"
size="w-25 w-min-200"
required
/>
</FormRow>
</div>
<div class="form-text evcc-gray">
{{
values.thresholds.enable.threshold === 0
? $t("config.loadpoint.thresholdEnableHelpZero", {
delay: fmtDurationNs(
values.thresholds.enable.delay,
true,
"m"
),
})
: values.thresholds.enable.threshold < 0
? $t(
"config.loadpoint.thresholdEnableHelpNegative",
{
surplus: fmtW(
-1 *
values.thresholds.enable
.threshold,
powerUnit.AUTO
),
delay: fmtDurationNs(
values.thresholds.enable.delay,
true,
"m"
),
}
)
: $t("config.loadpoint.thresholdEnableHelpInvalid")
}}
</div>
</div>
<div>
<div class="d-flex flex-wrap flex-sm-nowrap gap-4">
<FormRow
id="loadpointDisableThreshold"
:label="$t('config.loadpoint.thresholdDisableLabel')"
style="margin-bottom: 0 !important"
>
<PropertyField
id="loadpointDisableThreshold"
v-model="values.thresholds.disable.threshold"
type="Float"
unit="W"
size="w-25 w-min-200"
required
/>
</FormRow>
<FormRow
id="loadpointDisableDelay"
:label="$t('config.loadpoint.thresholdDisableDelayLabel')"
style="margin-bottom: 0 !important"
>
<PropertyField
id="loadpointDisableDelay"
v-model="values.thresholds.disable.delay"
type="Duration"
legacy-duration
unit="minute"
size="w-25 w-min-200"
required
/>
</FormRow>
</div>
<div class="form-text evcc-gray">
{{
values.thresholds.disable.threshold === 0
? $t("config.loadpoint.thresholdDisableHelpZero", {
delay: fmtDurationNs(
values.thresholds.disable.delay,
true,
"m"
),
})
: values.thresholds.disable.threshold > 0
? $t(
"config.loadpoint.thresholdDisableHelpPositive",
{
power: fmtW(
values.thresholds.disable.threshold,
powerUnit.AUTO
),
delay: fmtDurationNs(
values.thresholds.disable.delay,
true,
"m"
),
}
)
: $t("config.loadpoint.thresholdDisableHelpInvalid")
}}
</div>
</div>
</div>
<FormRow
v-if="showPriority"
id="loadpointParamPriority"
:label="$t('config.loadpoint.priorityLabel')"
:help="$t('config.loadpoint.priorityHelp')"
>
<PropertyField
id="loadpointParamPriority"
v-model="values.priority"
type="Choice"
size="w-100"
class="me-2"
required
:choice="priorityOptions"
/>
</FormRow>
<h6 v-if="!chargerIsSwitchDevice">
{{ $t("config.loadpoint.electricalTitle") }}
<small class="text-muted">{{
$t("config.loadpoint.electricalSubtitle")
}}</small>
</h6>
<FormRow
v-if="!chargerIsSwitchDevice"
id="chargerPower"
:label="$t('config.loadpoint.chargerTypeLabel')"
:help="
chargerPower === '11kw'
? $t('config.loadpoint.chargerPower11kwHelp')
: chargerPower === '22kw'
? $t('config.loadpoint.chargerPower22kwHelp')
: $t('config.loadpoint.chargerPowerCustomHelp')
"
>
<SelectGroup
id="chargerPower"
v-model="chargerPower"
class="w-100"
:options="[
{
name: $t('config.loadpoint.chargerPower11kw'),
value: '11kw',
},
{
name: $t('config.loadpoint.chargerPower22kw'),
value: '22kw',
},
{
name: $t('config.loadpoint.chargerPowerCustom'),
value: 'other',
},
]"
transparent
/>
</FormRow>
<div
v-if="!chargerIsSwitchDevice && chargerPower === 'other'"
class="row ms-3 mb-5"
>
<FormRow
id="loadpointMinCurrent"
:label="$t('config.loadpoint.minCurrentLabel')"
class="col-sm-6 mb-sm-0"
:help="
values.minCurrent < 6
? $t('config.loadpoint.minCurrentHelp')
: undefined
"
>
<PropertyField
id="loadpointMinCurrent"
v-model="values.minCurrent"
type="Float"
unit="A"
size="w-25 w-min-200"
class="me-2"
required
/>
</FormRow>
<FormRow
id="loadpointMaxCurrent"
:label="$t('config.loadpoint.maxCurrentLabel')"
class="col-sm-6 mb-sm-0"
:help="
values.maxCurrent < values.minCurrent
? $t('config.loadpoint.maxCurrentHelp')
: undefined
"
>
<PropertyField
id="loadpointMaxCurrent"
v-model="values.maxCurrent"
type="Float"
unit="A"
size="w-25 w-min-200"
class="me-2"
required
/>
</FormRow>
</div>
<template v-if="!chargerIsSinglePhase">
<FormRow
v-if="chargerSupports1p3p"
id="loadpointParamPhases"
:label="$t('config.loadpoint.phasesAutomatic')"
:help="$t('config.loadpoint.phasesAutomaticHelp')"
>
</FormRow>
<FormRow
v-else
id="loadpointParamPhases"
:label="$t('config.loadpoint.phasesLabel')"
:help="$t('config.loadpoint.phasesHelp')"
>
<SelectGroup
id="loadpointParamPhases"
v-model="values.phasesConfigured"
class="w-100"
:options="phasesOptions"
transparent
equal-width
/>
</FormRow>
</template>
<div v-if="showCircuit">
<FormRow
id="loadpointParamCircuit"
:label="$t('config.loadpoint.circuitLabel')"
:help="$t('config.loadpoint.circuitHelp')"
>
<InvalidReferenceAlert
v-if="invalidCircuit"
:message="$t('config.loadpoint.circuitInvalid')"
:value="values.circuit"
@remove="values.circuit = ''"
/>
<PropertyField
v-else
id="loadpointParamCircuit"
v-model="values.circuit"
type="Choice"
class="me-2"
:choice="circuitOptions"
/>
</FormRow>
</div>
<div v-if="!chargerIsIntegratedDevice">
<h6>{{ $t("config.loadpoint.vehiclesTitle") }}</h6>
<InvalidReferenceAlert
v-if="invalidVehicle"
:message="$t('config.loadpoint.vehicleInvalid')"
:value="values.vehicle"
@remove="values.vehicle = ''"
/>
<div v-else-if="vehicleOptions.length">
<FormRow
id="loadpointParamVehicle"
:label="$t('config.loadpoint.vehicleLabel')"
:help="
values.vehicle
? $t('config.loadpoint.vehicleHelpDefault')
: $t('config.loadpoint.vehicleHelpAutoDetection')
"
>
<PropertyField
id="loadpointParamVehicle"
v-model="values.vehicle"
type="Choice"
class="me-2"
:choice="allVehicleOptions"
/>
</FormRow>
<FormRow
id="loadpointPollMode"
:label="$t('config.loadpoint.pollModeLabel')"
:help="
values.soc.poll.mode === 'charging'
? $t('config.loadpoint.pollModeChargingHelp')
: values.soc.poll.mode === 'connected'
? $t('config.loadpoint.pollModeConnectedHelp')
: values.soc.poll.mode === 'always'
? $t('config.loadpoint.pollModeAlwaysHelp')
: undefined
"
>
<SelectGroup
id="loadpointPollMode"
v-model="values.soc.poll.mode"
class="w-100"
:options="[
{
value: 'charging',
name: $t('config.loadpoint.pollModeCharging'),
},
{
value: 'connected',
name: $t('config.loadpoint.pollModeConnected'),
},
{
value: 'always',
name: $t('config.loadpoint.pollModeAlways'),
},
]"
transparent
/>
</FormRow>
<FormRow
v-if="values.soc.poll.mode !== 'charging'"
id="loadpointPollInterval"
class="ms-3 mb-5"
:label="$t('config.loadpoint.pollIntervalLabel')"
:help="$t('config.loadpoint.pollIntervalHelp')"
:danger="$t('config.loadpoint.pollIntervalDanger')"
>
<PropertyField
id="loadpointPollInterval"
v-model="values.soc.poll.interval"
type="Duration"
legacy-duration
unit="minute"
size="w-25 w-min-200"
class="me-2"
required
/>
</FormRow>
<div>
<div class="d-flex mb-4">
<input
id="loadpointEstimate"
v-model="values.soc.estimate"
class="form-check-input"
type="checkbox"
/>
<label
class="form-check-label ms-2"
for="loadpointEstimate"
>
{{ $t("config.loadpoint.estimateLabel") }}
</label>
</div>
</div>
</div>
<div v-else>
<p class="text-muted">{{ $t("config.loadpoint.noVehicles") }}</p>
</div>
</div>
<div v-if="chargerIsHeating">
<h6>{{ $t("config.loadpoint.temperatureRangeTitle") }}</h6>
<p class="text-muted">
{{ $t("config.loadpoint.temperatureRangeHelp") }}
</p>
<div class="row">
<FormRow
id="loadpointMinTemp"
:label="$t('config.loadpoint.minTempLabel')"
class="col-sm-6 mb-sm-0"
>
<PropertyField
id="loadpointMinTemp"
v-model="values.ui.minTemp"
type="Float"
unit="°C"
size="w-25 w-min-200"
class="me-2"
/>
</FormRow>
<FormRow
id="loadpointMaxTemp"
:label="$t('config.loadpoint.maxTempLabel')"
class="col-sm-6 mb-sm-0"
:help="
values.ui.maxTemp <= values.ui.minTemp
? $t('config.loadpoint.maxTempHelp')
: undefined
"
>
<PropertyField
id="loadpointMaxTemp"
v-model="values.ui.maxTemp"
type="Float"
unit="°C"
size="w-25 w-min-200"
class="me-2"
/>
</FormRow>
</div>
</div>
</div>
</div>
</div>
<div
v-if="values.charger"
class="mt-5 mb-4 d-flex flex-wrap justify-content-between align-items-center gap-2"
>
<div class="d-flex flex-wrap align-items-center gap-1">
<button
v-if="isDeletable"
type="button"
class="btn btn-link text-danger"
@click.prevent="remove"
>
{{ $t("config.meter.delete") }}
</button>
<button
v-else
type="button"
class="btn btn-link text-muted btn-cancel"
data-bs-dismiss="modal"
>
{{ $t("config.loadpoint.cancel") }}
</button>
<button
v-if="isDeletable"
type="button"
class="btn btn-link text-muted"
@click.prevent="handleDisable(!isDisabled)"
>
{{
isDisabled ? $t("config.general.enable") : $t("config.general.disable")
}}
</button>
</div>
<button type="submit" class="btn btn-primary" :disabled="saving">
<span
v-if="saving"
class="spinner-border spinner-border-sm"
role="status"
aria-hidden="true"
></span>
{{ $t("config.loadpoint.save") }}
</button>
</div>
</form>
</GenericModal>
</template>
<script lang="ts">
import type { PropType } from "vue";
import FormRow from "./FormRow.vue";
import PropertyField from "./PropertyField.vue";
import SelectGroup from "../Helper/SelectGroup.vue";
import api from "@/api";
import GenericModal from "../Helper/GenericModal.vue";
import deepClone from "@/utils/deepClone";
import deepEqual from "@/utils/deepEqual";
import sleep from "@/utils/sleep";
import formatter, { POWER_UNIT } from "@/mixins/formatter";
import DeviceRefBox from "./DeviceRefBox.vue";
import NewDeviceButton from "./NewDeviceButton.vue";
import InvalidReferenceAlert from "./InvalidReferenceAlert.vue";
import CreateFlowStatus from "./CreateFlowStatus.vue";
import { handleError, customChargerName, createDeviceUtils } from "./DeviceModal";
import { getModal, openModal, replaceModal, closeModal } from "@/configModal";
import {
CHARGE_MODE,
LOADPOINT_TYPE,
type DeviceType,
type LoadpointType,
type ConfigCharger,
type ConfigMeter,
type VehicleOption,
type ConfigCircuit,
type ConfigLoadpoint,
} from "@/types/evcc";
const nsPerMin = 60 * 1e9;
const { OFF, PV, MINPV, NOW } = CHARGE_MODE;
const defaultValues = {
id: undefined,
title: "",
phasesConfigured: 3,
minCurrent: 6,
maxCurrent: 16,
priority: 0,
defaultMode: "",
thresholds: {
enable: { delay: 1 * nsPerMin, threshold: 0 },
disable: { delay: 3 * nsPerMin, threshold: 0 },
},
soc: {
poll: { mode: "charging", interval: 60 * nsPerMin },
estimate: true,
},
ui: {
minTemp: 0,
maxTemp: 100,
},
vehicle: "",
charger: "",
circuit: "",
meter: "",
} as ConfigLoadpoint;
const defaultThresholds = {
enable: { delay: 1 * nsPerMin, threshold: 0 },
disable: { delay: 3 * nsPerMin, threshold: 0 },
};
export default {
name: "LoadpointModal",
components: {
FormRow,
PropertyField,
GenericModal,
SelectGroup,
DeviceRefBox,
NewDeviceButton,
InvalidReferenceAlert,
CreateFlowStatus,
},
mixins: [formatter],
props: {
vehicleOptions: { type: Array as PropType<VehicleOption[]>, default: () => [] },
loadpointCount: { type: Number, default: 0 },
chargers: { type: Array as PropType<ConfigCharger[]>, default: () => [] },
chargerValues: { type: Object, default: () => {} },
meters: { type: Array as PropType<ConfigMeter[]>, default: () => [] },
circuits: { type: Array as PropType<ConfigCircuit[]>, default: () => [] },
hasDeviceError: {
type: Function as PropType<(type: DeviceType, name: string) => boolean>,
default: () => false,
},
},
emits: ["changed", "dismissed", "disable"],
data() {
return {
isModalVisible: false,
saving: false,
values: deepClone(defaultValues) as ConfigLoadpoint,
baseline: JSON.stringify(defaultValues),
chargerPower: "11kw",
solarMode: "default",
autoCreate: false,
autoCreateInProgress: false,
tab: "solar",
powerUnit: POWER_UNIT,
};
},
computed: {
id(): number | undefined {
return getModal("loadpoint")?.id;
},
dirty(): boolean {
return JSON.stringify(this.values) !== this.baseline;
},
selectedType(): LoadpointType | undefined {
return getModal("loadpoint")?.type as LoadpointType | undefined;
},
modalTitle() {
if (this.autoCreate && !this.autoCreateInProgress) {
return this.$t(`config.loadpoint.createdTitle.${this.loadpointType}`);
}
if (this.isNew || this.autoCreate) {
return this.$t(`config.loadpoint.titleAdd.${this.loadpointType || "unknown"}`);
}
return this.$t(`config.loadpoint.titleEdit.${this.loadpointType || "unknown"}`);
},
isNew() {
return this.id === undefined;
},
charger() {
return this.chargers.find((c) => c.name === this.values.charger);
},
chargerType() {
if (!this.charger) {
return null;
}
return this.chargerIsHeating ? LOADPOINT_TYPE.HEATING : LOADPOINT_TYPE.CHARGING;
},
chargerTitle() {
if (!this.charger) return "";
const title =
this.charger.deviceProduct ||
this.charger.config?.template ||
this.$t(customChargerName(this.charger.type, this.chargerIsHeating));
return title;
},
chargerStatus() {
if (!this.chargerValues || !this.values.charger) {
return null;
}
return this.chargerValues[this.values.charger] || null;
},
chargerSupports1p3p() {
return this.chargerStatus?.phases1p3p?.value || false;
},
chargerIsSinglePhase() {
return this.chargerStatus?.singlePhase?.value || false;
},
chargerIsIntegratedDevice() {
return this.chargerStatus?.integratedDevice?.value || false;
},
chargerIsSwitchDevice() {
return this.chargerStatus?.switchDevice?.value || false;
},
chargerIsHeating() {
return this.chargerStatus?.heating?.value === true;
},
meterTitle() {
const name = this.values.meter;
if (!name) return "";
const meter = this.meters.find((m) => m.name === name);
const title =
meter?.deviceProduct ||
meter?.config?.template ||
this.$t("config.general.customOption");
return title;
},
isDeletable() {
return !this.isNew;
},
isDisabled() {
return Boolean(this.values.disable);
},
showPriority() {
return this.isNew ? this.loadpointCount > 0 : this.loadpointCount > 1;
},
priorityOptions() {
const result = Array.from({ length: 11 }, (_, i) => ({ key: i, name: `${i}` })) as {
key?: number;
name: string;
}[];
result[0]!.name = "0 (default)";
result[10]!.name = "10 (highest)";
return result;
},
phasesOptions() {
return [
{ value: 1, name: this.$t("config.loadpoint.phases1p") },
{ value: 3, name: this.$t("config.loadpoint.phases3p") },
];
},
defaultModeOptions(): { key: CHARGE_MODE; name: string }[] {
// empty option is provided by PropertyField placeholder
// switch devices have no current control, so minpv does not apply
const modes = this.chargerIsSwitchDevice ? [OFF, PV, NOW] : [OFF, PV, MINPV, NOW];
return modes.map((key) => ({ key, name: this.$t(`main.mode.${key}`) }));
},
showCircuit() {
return this.circuits.length > 0 || !!this.values.circuit;
},
invalidCircuit() {
const { circuit } = this.values;
return circuit && !this.circuitOptions.some((c) => c.key === circuit);
},
circuitOptions() {
// empty option is provided by PropertyField placeholder
return this.circuits.map((c) => ({
key: c.name,
name: `${c.config?.title || ""} [${c.name}]`.trim(),
}));
},
invalidVehicle() {
const { vehicle } = this.values;
return vehicle && !this.vehicleOptions.some(({ key }) => key === vehicle);
},
allVehicleOptions() {
return [
{ key: "", name: this.$t("config.loadpoint.vehicleAutoDetection") },
{ key: null, name: null },
...this.vehicleOptions,
];
},
typeChoices(): LoadpointType[] {
return Object.values(LOADPOINT_TYPE);
},
loadpointType(): LoadpointType | null {
return this.selectedType ?? this.chargerType;
},
addChargerLabel() {
if (this.loadpointType) {
return this.$t(`config.loadpoint.addCharger.${this.loadpointType}`);
}
return "";
},
},
watch: {
isModalVisible(visible) {
if (visible) {
if (this.values?.id !== this.id) {
// loadpoint changed
this.reset();
if (this.id) {
this.loadConfiguration();
}
}
}
},
id(newId) {
// replaceModal swaps the id in place (after create) without a remount
if (this.isModalVisible && newId !== undefined && this.values?.id !== newId) {
this.loadConfiguration();
}
},
chargerPower(value) {
if (value === "11kw") {
this.values.minCurrent = 6;
this.values.maxCurrent = 16;
} else if (value === "22kw") {
this.values.minCurrent = 6;
this.values.maxCurrent = 32;
}
},
solarMode(value) {
if (value === "default") {
this.values.thresholds = deepClone(defaultThresholds);
}
},
chargerSupports1p3p() {
this.updatePhases();
},
chargerIsSinglePhase() {
this.updatePhases();
},
},
methods: {
reset() {
this.values = deepClone(defaultValues);
this.autoCreate = false;
this.autoCreateInProgress = false;
this.updatePhases();
this.rebaseline();
},
rebaseline() {
this.baseline = JSON.stringify(this.values);
},
async loadConfiguration() {
try {
const res = await api.get(`config/loadpoints/${this.id}`);
this.values = deepClone(res.data);
this.updateChargerPower();
this.updateSolarMode();
this.updatePhases();
this.rebaseline();
} catch (e) {
console.error(e);
}
},
async emitChanged(action: "added" | "updated" | "removed") {
const result = { action };
await closeModal(result);
this.$emit("changed", result);
},
async update() {
this.saving = true;
try {
const values = deepClone(this.values);
await api.put(`config/loadpoints/${this.id}`, values);
this.emitChanged("updated");
} catch (e) {
handleError(e, "update failed");
}
this.saving = false;
},
async remove() {
try {
await api.delete(`config/loadpoints/${this.id}`);
this.emitChanged("removed");
} catch (e) {
console.error(e);
alert("delete failed");
}
},
async handleDisable(disable: boolean) {
if (this.id === undefined) return;
this.$emit("disable", { id: this.id, disable });
await closeModal();
},
async create() {
this.autoCreate = true;
this.autoCreateInProgress = true;
await sleep(1000); // ux pause
try {
for (let i = 0; !this.chargerStatus && i < 100; i++) {
await sleep(50);
}
const res = await api.post("config/loadpoints", this.values);
this.$emit("changed", { action: "added" });
// edit mode of the newly loadpoint
replaceModal("loadpoint", {
id: res.data?.id,
type: this.loadpointType || undefined,
});
this.autoCreateInProgress = false;
} catch (e) {
this.autoCreate = false;
this.autoCreateInProgress = false;
handleError(e, "create failed");
}
},
async finish() {
await closeModal({ action: "added" });
this.reset();
},
onOpen() {
this.isModalVisible = true;
},
onClose() {
this.isModalVisible = false;
},
async onDismiss() {
// closeModal already cleared the route id, so check values.id, not isNew
if (!this.values.id && !this.autoCreate) {
await this.cleanupDevice("charger", this.values.charger, this.chargers);
await this.cleanupDevice("meter", this.values.meter, this.meters);
this.$emit("dismissed");
this.reset();
return;
}
// keep values so reopening the same loadpoint stays populated
this.autoCreate = false;
this.autoCreateInProgress = false;
},
async cleanupDevice(type: DeviceType, name: string, list: { name: string; id: number }[]) {
const id = list.find((d) => d.name === name)?.id;
if (id === undefined) return;
try {
await createDeviceUtils(type).remove(id);
} catch (e) {
console.error(e);
}
},
async editCharger() {
const charger = this.chargers.find((c) => c.name === this.values.charger);
const result = await openModal("charger", {
id: charger?.id,
type: this.loadpointType || undefined,
});
if (result.action === "added" && result.name) {
const instantCreate = this.isNew && !this.values.charger;
this.values.charger = result.name;
if (instantCreate) {
await this.create();
}
} else if (result.action === "removed") {
this.values.charger = "";
}
},
async editMeter() {
const meter = this.meters.find((m) => m.name === this.values.meter);
const result = await openModal("meter", {
id: meter?.id,
type: "charge",
});
if (result.action === "added" && result.name) {
this.values.meter = result.name;
} else if (result.action === "removed") {
this.values.meter = "";
}
},
updateChargerPower() {
const { minCurrent, maxCurrent } = this.values;
if (minCurrent === 6 && maxCurrent === 16) {
this.chargerPower = "11kw";
} else if (minCurrent === 6 && maxCurrent === 32) {
this.chargerPower = "22kw";
} else {
this.chargerPower = "other";
}
},
updateSolarMode() {
const { thresholds } = this.values;
if (deepEqual(thresholds, defaultThresholds)) {
this.solarMode = "default";
} else {
this.solarMode = "custom";
}
},
updatePhases() {
const { phasesConfigured } = this.values;
if (this.chargerIsSinglePhase) {
this.values.phasesConfigured = 1;
return;
}
if (this.chargerSupports1p3p && this.isNew) {
this.values.phasesConfigured = 0; // automatic
return;
}
if (!this.chargerSupports1p3p && phasesConfigured === 0) {
this.values.phasesConfigured = 3; // no automatic switching, default to 3-phase
return;
}
},
selectType(type: LoadpointType) {
replaceModal("loadpoint", { id: this.id, type });
},
},
};
</script>
<style scoped>
.container {
margin-left: calc(var(--bs-gutter-x) * -0.5);
margin-right: calc(var(--bs-gutter-x) * -0.5);
padding-right: 0;
}
.addButton {
min-height: auto;
}
h6 {
margin-top: 4rem;
}
</style>