evcc-io/assets/js/components/Loadpoints/SettingsModal.vue

378 lines
11 KiB
Vue

<template>
<GenericModal
:id="`loadpointSettingsModal_${id}`"
ref="modal"
:title="$t('main.loadpointSettings.title', [loadpoint?.title])"
size="xl"
data-testid="loadpoint-settings-modal"
@open="modalVisible"
@closed="modalInvisible"
>
<div class="container">
<SmartCostLimit
:current-limit="loadpoint?.smartCostLimit ?? null"
:last-limit="loadpoint?.lastSmartCostLimit"
:smart-cost-type="smartCostType"
:currency="currency"
is-loadpoint
:loadpoint-id="id"
:multiple-loadpoints="multipleLoadpoints"
:possible="smartCostAvailable"
:tariff="forecast?.planner"
class="mt-2 mb-4"
/>
<SmartFeedInPriority
:current-limit="loadpoint?.smartFeedInPriorityLimit ?? null"
:last-limit="loadpoint?.lastSmartFeedInPriorityLimit"
:currency="currency"
:loadpoint-id="id"
:multiple-loadpoints="multipleLoadpoints"
:possible="smartFeedInPriorityAvailable"
:tariff="forecast?.feedin"
class="mt-2 mb-4"
/>
<LoadpointSettingsBatteryBoost
v-if="batteryBoostAvailable"
v-bind="batteryBoostProps"
class="mt-2"
@batteryboostlimit-updated="setBatteryBoostLimit"
/>
<h6 v-if="heating">
{{ $t("main.loadpointSettings.heating") }}
</h6>
<div v-if="heating" class="mb-3 row">
<label :for="formId('mintemp')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ $t("main.loadpointSettings.minTemp.label") }}
</label>
<div class="col-sm-8 col-lg-4 pe-0 d-flex align-items-center">
<select
:id="formId('mintemp')"
v-model.number="selectedMinTemp"
class="form-select form-select-sm"
@change="setMinTemp"
>
<option
v-for="{ value, name } in minTempOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
</div>
<div class="col-sm-8 offset-sm-4 mt-1">
<small class="text-muted">
{{
$t("main.loadpointSettings.minTemp.description", [
selectedMinTemp ? fmtTemperature(selectedMinTemp) : "x °C",
])
}}
</small>
</div>
</div>
<h6>
{{ $t("main.loadpointSettings.currents") }}
</h6>
<div v-if="phasesOptions.length" class="mb-3 row">
<label
:for="formId(`phases_${phasesOptions[0]}`)"
class="col-sm-4 col-form-label pt-0"
>
{{ $t("main.loadpointSettings.phasesConfigured.label") }}
</label>
<div class="col-sm-8 pe-0">
<p v-if="!loadpoint?.chargerPhases1p3p" class="mt-0 mb-2">
<small>
{{ $t("main.loadpointSettings.phasesConfigured.no1p3pSupport") }}</small
>
</p>
<div v-for="phases in phasesOptions" :key="phases" class="form-check">
<input
:id="formId(`phases_${phases}`)"
v-model.number="selectedPhases"
class="form-check-input"
type="radio"
:name="formId('phases')"
:value="phases"
@change="setPhasesConfigured"
/>
<label class="form-check-label" :for="formId(`phases_${phases}`)">
{{ $t(`main.loadpointSettings.phasesConfigured.phases_${phases}`) }}
<small v-if="phases > 0">
{{
$t(
`main.loadpointSettings.phasesConfigured.phases_${phases}_hint`,
{
min: fmtPhasePower(minCurrent, phases),
max: fmtPhasePower(maxCurrent, phases),
}
)
}}
</small>
</label>
</div>
</div>
</div>
<div v-if="!switchDevice" class="mb-3 row">
<label :for="formId('maxcurrent')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ $t("main.loadpointSettings.maxCurrent.label") }}
</label>
<div class="col-sm-8 col-lg-4 pe-0 d-flex align-items-center">
<select
:id="formId('maxcurrent')"
v-model.number="selectedMaxCurrent"
class="form-select form-select-sm"
@change="setMaxCurrent"
>
<option
v-for="{ value, name } in maxCurrentOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label :for="formId('mincurrent')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ $t("main.loadpointSettings.minCurrent.label") }}
</label>
<div class="col-sm-8 col-lg-4 pe-0 d-flex align-items-center">
<select
:id="formId('mincurrent')"
v-model.number="selectedMinCurrent"
class="form-select form-select-sm"
@change="setMinCurrent"
>
<option
v-for="{ value, name } in minCurrentOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
</div>
</div>
</div>
</GenericModal>
</template>
<script lang="ts">
import collector from "@/mixins/collector.ts";
import formatter from "@/mixins/formatter";
import GenericModal from "../Helper/GenericModal.vue";
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 UiForecast, type UiLoadpoint } from "@/types/evcc";
import api from "@/api";
const V = 230;
const range = (start: number, stop: number, step = -1) =>
Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
const insertSorted = (arr: number[], num: number) => {
const uniqueSet = new Set(arr);
uniqueSet.add(num);
return [...uniqueSet].sort((a, b) => b - a);
};
// TODO: add max physical current to loadpoint (config ui) and only allow user to select values in side that range (main ui, here)
const MAX_CURRENT = 64;
const { AUTO, THREE_PHASES, ONE_PHASE } = PHASES;
export default defineComponent({
name: "LoadpointSettingsModal",
components: {
GenericModal,
SmartCostLimit,
SmartFeedInPriority,
LoadpointSettingsBatteryBoost: SettingsBatteryBoost,
},
mixins: [formatter, collector],
props: {
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
batteryConfigured: Boolean,
smartCostType: String as PropType<SMART_COST_TYPE>,
smartCostAvailable: Boolean,
smartFeedInPriorityAvailable: Boolean,
tariffGrid: Number,
currency: String as PropType<CURRENCY>,
multipleLoadpoints: Boolean,
forecast: Object as PropType<UiForecast>,
},
data() {
return {
id: undefined as string | undefined,
selectedMaxCurrent: undefined as number | undefined,
selectedMinCurrent: undefined as number | undefined,
selectedMinTemp: 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;
},
switchDevice() {
return this.loadpoint?.chargerFeatureSwitchDevice;
},
heating() {
return this.loadpoint?.chargerFeatureHeating;
},
minTemp() {
// stored as loadpoint minSoc, interpreted as temperature for heating devices
return this.loadpoint?.minSoc;
},
minTempOptions() {
let { minTemp = 0, maxTemp = 100 } = this.loadpoint?.ui ?? {};
if (minTemp >= maxTemp) {
minTemp = 0;
maxTemp = 100;
}
return [0, ...range(Math.max(minTemp, 1), maxTemp, 1)].map((value) => ({
value,
name: value === 0 ? "---" : this.fmtTemperature(value),
}));
},
batteryBoostLimit() {
return this.loadpoint?.batteryBoostLimit;
},
phasesConfigured() {
return this.loadpoint?.phasesConfigured;
},
phasesOptions() {
if (this.loadpoint?.chargerSinglePhase) {
return [];
}
if (this.loadpoint?.chargerPhases1p3p) {
// automatic switching
return [AUTO, THREE_PHASES, ONE_PHASE];
}
// 1p or 3p possible
return [THREE_PHASES, ONE_PHASE];
},
batteryBoostProps() {
return this.collectProps(SettingsBatteryBoost);
},
maxPhases() {
if (this.loadpoint?.chargerPhases1p3p && this.phasesConfigured === AUTO) {
return THREE_PHASES;
}
return this.phasesConfigured;
},
minPhases() {
if (this.loadpoint?.chargerPhases1p3p && this.phasesConfigured === AUTO) {
return ONE_PHASE;
}
return this.phasesConfigured;
},
minCurrentOptions() {
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 ?? 0);
return opt2.map((value) => this.currentOption(value, value === 6, this.minPhases));
},
maxCurrentOptions() {
const opt1 = range(MAX_CURRENT, Math.ceil(this.minCurrent ?? 0));
// ensure that current value is always included
const opt2 = insertSorted(opt1, this.maxCurrent ?? 0);
return opt2.map((value) => this.currentOption(value, value === 16, this.maxPhases));
},
batteryBoostAvailable() {
return this.batteryConfigured;
},
},
watch: {
maxCurrent(value) {
this.selectedMaxCurrent = value;
},
minCurrent(value) {
this.selectedMinCurrent = value;
},
minTemp(value) {
this.selectedMinTemp = value;
},
phasesConfigured(value) {
this.selectedPhases = value;
},
},
methods: {
open(loadpointId: string) {
this.id = loadpointId;
this.selectedPhases = this.phasesConfigured;
this.selectedMaxCurrent = this.maxCurrent;
this.selectedMinCurrent = this.minCurrent;
this.selectedMinTemp = this.minTemp;
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}`;
},
setMaxCurrent() {
api.post(this.apiPath("maxcurrent") + "/" + this.selectedMaxCurrent);
},
setMinCurrent() {
api.post(this.apiPath("mincurrent") + "/" + this.selectedMinCurrent);
},
setMinTemp() {
api.post(this.apiPath("mintemp") + "/" + this.selectedMinTemp);
},
setPhasesConfigured() {
api.post(this.apiPath("phases") + "/" + this.selectedPhases);
},
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) {
name += ` [${this.$t("main.loadpointSettings.default")}]`;
}
return { value: current, name };
},
modalVisible() {
this.isModalVisible = true;
},
modalInvisible() {
this.isModalVisible = false;
},
},
});
</script>
<style scoped>
.container {
margin-left: calc(var(--bs-gutter-x) * -0.5);
margin-right: calc(var(--bs-gutter-x) * -0.5);
}
.container h4:first-child {
margin-top: 0 !important;
}
.custom-select-inline {
display: inline-block !important;
}
</style>