393 lines
19 KiB
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>
|