OpenDTU-old/webapp/src/views/PowerLimiterAdminView.vue

393 lines
19 KiB
Vue

<template>
<BasePage :title="$t('powerlimiteradmin.PowerLimiterSettings')" :isLoading="dataLoading">
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
{{ alertMessage }}
</BootstrapAlert>
<BootstrapAlert v-model="configAlert" variant="warning">
{{ $t('powerlimiteradmin.ConfigAlertMessage') }}
</BootstrapAlert>
<CardElement :text="$t('powerlimiteradmin.ConfigHints')" textVariant="text-bg-primary" v-if="getConfigHints().length">
<div class="row">
<div class="col-sm-12">
{{ $t('powerlimiteradmin.ConfigHintsIntro') }}
<ul class="mb-0">
<li v-for="(hint, idx) in getConfigHints()" :key="idx">
<b v-if="hint.severity === 'requirement'">{{ $t('powerlimiteradmin.ConfigHintRequirement') }}:</b>
<b v-if="hint.severity === 'optional'">{{ $t('powerlimiteradmin.ConfigHintOptional') }}:</b>
{{ $t('powerlimiteradmin.ConfigHint' + hint.subject) }}
</li>
</ul>
</div>
</div>
</CardElement>
<form @submit="savePowerLimiterConfig" v-if="!configAlert">
<CardElement :text="$t('powerlimiteradmin.General')" textVariant="text-bg-primary" add-space>
<InputElement :label="$t('powerlimiteradmin.Enable')"
v-model="powerLimiterConfigList.enabled"
type="checkbox" wide/>
<InputElement v-show="isEnabled()"
:label="$t('powerlimiteradmin.VerboseLogging')"
v-model="powerLimiterConfigList.verbose_logging"
type="checkbox" wide/>
<InputElement v-show="isEnabled() && hasPowerMeter()"
:label="$t('powerlimiteradmin.TargetPowerConsumption')"
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHint')"
v-model="powerLimiterConfigList.target_power_consumption"
postfix="W"
type="number" wide/>
<InputElement v-show="isEnabled()"
:label="$t('powerlimiteradmin.TargetPowerConsumptionHysteresis')"
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHysteresisHint')"
v-model="powerLimiterConfigList.target_power_consumption_hysteresis"
postfix="W"
type="number" wide/>
</CardElement>
<CardElement :text="$t('powerlimiteradmin.InverterSettings')" textVariant="text-bg-primary" add-space v-if="isEnabled()">
<div class="row mb-3">
<label for="inverter_serial" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.Inverter') }}
</label>
<div class="col-sm-8">
<select id="inverter_serial" class="form-select" v-model="powerLimiterConfigList.inverter_serial" required>
<option value="" disabled hidden selected>{{ $t('powerlimiteradmin.SelectInverter') }}</option>
<option v-for="(inv, serial) in powerLimiterMetaData.inverters" :key="serial" :value="serial">
{{ inv.name }} ({{ inv.type }})
</option>
</select>
</div>
</div>
<InputElement :label="$t('powerlimiteradmin.InverterIsSolarPowered')"
v-model="powerLimiterConfigList.is_inverter_solar_powered"
type="checkbox" wide/>
<div class="row mb-3" v-if="needsChannelSelection()">
<label for="inverter_channel" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.InverterChannelId') }}
</label>
<div class="col-sm-8">
<select id="inverter_channel" class="form-select" v-model="powerLimiterConfigList.inverter_channel_id">
<option v-for="channel in range(powerLimiterMetaData.inverters[powerLimiterConfigList.inverter_serial].channels)" :key="channel" :value="channel">
{{ channel + 1 }}
</option>
</select>
</div>
</div>
<InputElement :label="$t('powerlimiteradmin.LowerPowerLimit')"
:tooltip="$t('powerlimiteradmin.LowerPowerLimitHint')"
v-model="powerLimiterConfigList.lower_power_limit"
placeholder="50" min="10" postfix="W"
type="number" wide/>
<InputElement :label="$t('powerlimiteradmin.BaseLoadLimit')"
:tooltip="$t('powerlimiteradmin.BaseLoadLimitHint')"
v-model="powerLimiterConfigList.base_load_limit"
placeholder="200" :min="(powerLimiterConfigList.lower_power_limit + 1).toString()" postfix="W"
type="number" wide/>
<InputElement :label="$t('powerlimiteradmin.UpperPowerLimit')"
v-model="powerLimiterConfigList.upper_power_limit"
:tooltip="$t('powerlimiteradmin.UpperPowerLimitHint')"
placeholder="800" :min="(powerLimiterConfigList.base_load_limit + 1).toString()" postfix="W"
type="number" wide/>
<InputElement v-show="hasPowerMeter()"
:label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')"
v-model="powerLimiterConfigList.is_inverter_behind_powermeter"
:tooltip="$t('powerlimiteradmin.InverterIsBehindPowerMeterHint')"
type="checkbox" wide/>
<div class="row mb-3" v-if="!powerLimiterConfigList.is_inverter_solar_powered">
<label for="inverter_restart" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.InverterRestartHour') }}
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterRestartHint')" />
</label>
<div class="col-sm-8">
<select id="inverter_restart" class="form-select" v-model="powerLimiterConfigList.inverter_restart_hour">
<option value="-1">
{{ $t('powerlimiteradmin.InverterRestartDisabled') }}
</option>
<option v-for="hour in range(24)" :key="hour" :value="hour">
{{ (hour > 9) ? hour : "0"+hour }}:00
</option>
</select>
</div>
</div>
</CardElement>
<CardElement :text="$t('powerlimiteradmin.SolarPassthrough')" textVariant="text-bg-primary" add-space v-if="canUseSolarPassthrough()">
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.SolarpassthroughInfo')"></div>
<InputElement :label="$t('powerlimiteradmin.EnableSolarPassthrough')"
v-model="powerLimiterConfigList.solar_passthrough_enabled"
type="checkbox" wide/>
<div v-if="powerLimiterConfigList.solar_passthrough_enabled">
<InputElement :label="$t('powerlimiteradmin.BatteryDischargeAtNight')"
v-model="powerLimiterConfigList.battery_always_use_at_night"
type="checkbox" wide/>
<InputElement :label="$t('powerlimiteradmin.SolarPassthroughLosses')"
v-model="powerLimiterConfigList.solar_passthrough_losses"
placeholder="3" min="0" max="10" postfix="%"
type="number" wide/>
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.SolarPassthroughLossesInfo')"></div>
</div>
</CardElement>
<CardElement :text="$t('powerlimiteradmin.SocThresholds')" textVariant="text-bg-primary" add-space v-if="canUseSoCThresholds()">
<InputElement
:label="$t('powerlimiteradmin.IgnoreSoc')"
v-model="powerLimiterConfigList.ignore_soc"
type="checkbox" wide/>
<div v-if="!powerLimiterConfigList.ignore_soc">
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.BatterySocInfo')"></div>
<InputElement :label="$t('powerlimiteradmin.StartThreshold')"
v-model="powerLimiterConfigList.battery_soc_start_threshold"
placeholder="80" min="0" max="100" postfix="%"
type="number" wide/>
<InputElement :label="$t('powerlimiteradmin.StopThreshold')"
v-model="powerLimiterConfigList.battery_soc_stop_threshold"
placeholder="20" min="0" max="100" postfix="%"
type="number" wide/>
<InputElement :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
v-model="powerLimiterConfigList.full_solar_passthrough_soc"
v-if="isSolarPassthroughEnabled()"
placeholder="80" min="0" max="100" postfix="%"
type="number" wide/>
</div>
</CardElement>
<CardElement :text="$t('powerlimiteradmin.VoltageThresholds')" textVariant="text-bg-primary" add-space v-if="canUseVoltageThresholds()">
<InputElement :label="$t('powerlimiteradmin.StartThreshold')"
v-model="powerLimiterConfigList.voltage_start_threshold"
placeholder="50" min="16" max="66" postfix="V"
type="number" step="0.01" wide/>
<InputElement :label="$t('powerlimiteradmin.StopThreshold')"
v-model="powerLimiterConfigList.voltage_stop_threshold"
placeholder="49" min="16" max="66" postfix="V"
type="number" step="0.01" wide/>
<div v-if="isSolarPassthroughEnabled()">
<InputElement :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
v-model="powerLimiterConfigList.full_solar_passthrough_start_voltage"
placeholder="49" min="16" max="66" postfix="V"
type="number" step="0.01" wide/>
<InputElement :label="$t('powerlimiteradmin.VoltageSolarPassthroughStopThreshold')"
v-model="powerLimiterConfigList.full_solar_passthrough_stop_voltage"
placeholder="49" min="16" max="66" postfix="V"
type="number" step="0.01" wide/>
</div>
<InputElement :label="$t('powerlimiteradmin.VoltageLoadCorrectionFactor')"
v-model="powerLimiterConfigList.voltage_load_correction_factor"
placeholder="0.0001" postfix="1/A"
type="number" step="0.0001" wide/>
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.VoltageLoadCorrectionInfo')"></div>
</CardElement>
<FormFooter @reload="getAllData"/>
</form>
</BasePage>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue';
import { BIconInfoCircle } from 'bootstrap-icons-vue';
import type { PowerLimiterConfig, PowerLimiterMetaData } from "@/types/PowerLimiterConfig";
export default defineComponent({
components: {
BasePage,
BootstrapAlert,
CardElement,
FormFooter,
InputElement,
BIconInfoCircle,
},
data() {
return {
dataLoading: true,
powerLimiterConfigList: {} as PowerLimiterConfig,
powerLimiterMetaData: {} as PowerLimiterMetaData,
alertMessage: "",
alertType: "info",
showAlert: false,
configAlert: false,
};
},
created() {
this.getAllData();
},
watch: {
'powerLimiterConfigList.inverter_serial'(newVal) {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
if (newVal === "") { return; } // do not try to convert the placeholder value
if (meta.inverters[newVal] !== undefined) { return; }
for (const [serial, inverter] of Object.entries(meta.inverters)) {
// cfg.inverter_serial might be too large to parse as a 32 bit
// int, so we make sure to only try to parse two characters. if
// cfg.inverter_serial is indeed an old position based index,
// it is only one character.
if (inverter.pos == Number(cfg.inverter_serial.substr(0, 2))) {
// inverter_serial uses the old position-based
// value to identify the inverter. convert to serial.
cfg.inverter_serial = serial;
return;
}
}
// previously selected inverter was deleted. marks serial as
// invalid, selects placeholder option.
cfg.inverter_serial = '';
}
},
methods: {
getConfigHints() {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
const hints = [];
if (meta.power_meter_enabled !== true) {
hints.push({severity: "optional", subject: "PowerMeterDisabled"});
}
if (typeof meta.inverters === "undefined" || Object.keys(meta.inverters).length == 0) {
hints.push({severity: "requirement", subject: "NoInverter"});
this.configAlert = true;
}
else {
const inv = meta.inverters[cfg.inverter_serial];
if (inv !== undefined && !(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) {
hints.push({severity: "requirement", subject: "InverterCommunication"});
}
}
if (!cfg.is_inverter_solar_powered) {
if (!meta.charge_controller_enabled) {
hints.push({severity: "optional", subject: "NoChargeController"});
}
if (!meta.battery_enabled) {
hints.push({severity: "optional", subject: "NoBatteryInterface"});
}
}
return hints;
},
isEnabled() {
return this.powerLimiterConfigList.enabled;
},
hasPowerMeter() {
return this.powerLimiterMetaData.power_meter_enabled;
},
canUseSolarPassthrough() {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
const canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered;
if (!canUse) { cfg.solar_passthrough_enabled = false; }
return canUse;
},
canUseSoCThresholds() {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
return this.isEnabled() && meta.battery_enabled && !cfg.is_inverter_solar_powered;
},
canUseVoltageThresholds() {
const cfg = this.powerLimiterConfigList;
return this.isEnabled() && !cfg.is_inverter_solar_powered;
},
isSolarPassthroughEnabled() {
return this.powerLimiterConfigList.solar_passthrough_enabled;
},
range(end: number) {
return Array.from(Array(end).keys());
},
needsChannelSelection() {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
const reset = function() {
cfg.inverter_channel_id = 0;
return false;
};
if (cfg.inverter_serial === '') { return reset(); }
if (cfg.is_inverter_solar_powered) { return reset(); }
const inverter = meta.inverters[cfg.inverter_serial];
if (inverter === undefined) { return reset(); }
if (cfg.inverter_channel_id >= inverter.channels) {
reset();
}
return inverter.channels > 1;
},
getAllData() {
this.dataLoading = true;
fetch("/api/powerlimiter/metadata", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.powerLimiterMetaData = data;
fetch("/api/powerlimiter/config", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.powerLimiterConfigList = data;
this.dataLoading = false;
});
});
},
savePowerLimiterConfig(e: Event) {
e.preventDefault();
const formData = new FormData();
formData.append("data", JSON.stringify(this.powerLimiterConfigList));
fetch("/api/powerlimiter/config", {
method: "POST",
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.alertMessage = response.message;
this.alertType = response.type;
this.showAlert = true;
}
);
},
},
});
</script>