this changeset refactors the web application's DPL settings view. the DPL settings can be complex, and they shall be presented in a way that allows users to comprehend their meaning. irrelevant settings are now hidden or displayed dynamically based on the influencing settings. * group SoC thresholds into their own card * hide battery SoC thresholds if battery disabled. if the user did not even enable the battery interface, battery SoC values will not be used for DPL decisions. in that case we completely hide the respective settings from the DPL admin view. this reduces the amount of settings for new users and especially users who don't even have a battery in their setup or have no BMS connected. * group voltage thresholds and improve label texts * fix load correction factor unit * fix header (wording) * group solar-passthrough settings in new card * group inverter-related settings * hide solar passthrough settings if VE.Direct is disabled. closes #662. * completely disable form if any requirement is not met * list available inverters by name and type. this makes it much more convenient to select the right inverter, especially since the order of the inverters in the web UI is decoupled from their position in the internal array, which was used to select them previously. care was taken that old configs select the same inverter after an update. when editing the DPL settings, the selects an inverter from the newly created drow-down list, and the respective old inverter is pre-selected. * disable form if no inverter is configured (config alert) * make inverter input selection dynamic. adjust selection to actual amount of channels for selected inverter. skip selection altogether if inverter has only one channel, or if it is solar powered. * web app: wording adjustments * group meta data into new property and exclude from submission. saves memory when evaluating the submitted settings. * hide irrelevant settings if inverter is solar-powered * move restart hour setting to inverter card. translate setting which disabled automatic restart. * simplify "drain strategy" setting into an on/off toggle. care was taken that existing configs work the same after an upgrade. the respective drain strategy is translated into the new setting when reading the config. once the config is written, the new setting is persisted and the old is not part of the config any more. * show more configuration hints, depending on actual configuration * replace inputs by InputElement components where possible
374 lines
18 KiB
Vue
374 lines
18 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()"
|
|
: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 powerLimiterConfigList.metadata.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(powerLimiterConfigList.metadata.inverters[powerLimiterConfigList.inverter_serial].channels)" :key="channel" :value="channel">
|
|
{{ channel + 1 }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<InputElement :label="$t('powerlimiteradmin.LowerPowerLimit')"
|
|
v-model="powerLimiterConfigList.lower_power_limit"
|
|
placeholder="50" min="10" postfix="W"
|
|
type="number" wide/>
|
|
|
|
<InputElement :label="$t('powerlimiteradmin.UpperPowerLimit')"
|
|
v-model="powerLimiterConfigList.upper_power_limit"
|
|
placeholder="800" min="20" postfix="W"
|
|
type="number" wide/>
|
|
|
|
<InputElement :label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')"
|
|
v-model="powerLimiterConfigList.is_inverter_behind_powermeter"
|
|
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="getPowerLimiterConfig"/>
|
|
</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 } from "@/types/PowerLimiterConfig";
|
|
|
|
export default defineComponent({
|
|
components: {
|
|
BasePage,
|
|
BootstrapAlert,
|
|
CardElement,
|
|
FormFooter,
|
|
InputElement,
|
|
BIconInfoCircle,
|
|
},
|
|
data() {
|
|
return {
|
|
dataLoading: true,
|
|
powerLimiterConfigList: {} as PowerLimiterConfig,
|
|
alertMessage: "",
|
|
alertType: "info",
|
|
showAlert: false,
|
|
configAlert: false,
|
|
};
|
|
},
|
|
created() {
|
|
this.getPowerLimiterConfig();
|
|
},
|
|
watch: {
|
|
'powerLimiterConfigList.inverter_serial'(newVal) {
|
|
var cfg = this.powerLimiterConfigList;
|
|
|
|
if (newVal === "") { return; } // do not try to convert the placeholder value
|
|
|
|
if (cfg.metadata.inverters[newVal] !== undefined) { return; }
|
|
|
|
for (const [serial, inverter] of Object.entries(cfg.metadata.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() {
|
|
var cfg = this.powerLimiterConfigList;
|
|
var hints = [];
|
|
|
|
if (cfg.metadata.power_meter_enabled !== true) {
|
|
hints.push({severity: "requirement", subject: "PowerMeterDisabled"});
|
|
this.configAlert = true;
|
|
}
|
|
|
|
if (typeof cfg.metadata.inverters === "undefined" || Object.keys(cfg.metadata.inverters).length == 0) {
|
|
hints.push({severity: "requirement", subject: "NoInverter"});
|
|
this.configAlert = true;
|
|
}
|
|
else {
|
|
var inv = cfg.metadata.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 (!cfg.metadata.charge_controller_enabled) {
|
|
hints.push({severity: "optional", subject: "NoChargeController"});
|
|
}
|
|
|
|
if (!cfg.metadata.battery_enabled) {
|
|
hints.push({severity: "optional", subject: "NoBatteryInterface"});
|
|
}
|
|
}
|
|
|
|
return hints;
|
|
},
|
|
isEnabled() {
|
|
return this.powerLimiterConfigList.enabled;
|
|
},
|
|
canUseSolarPassthrough() {
|
|
var cfg = this.powerLimiterConfigList;
|
|
var canUse = this.isEnabled() && cfg.metadata.charge_controller_enabled && !cfg.is_inverter_solar_powered;
|
|
if (!canUse) { cfg.solar_passthrough_enabled = false; }
|
|
return canUse;
|
|
},
|
|
canUseSoCThresholds() {
|
|
var cfg = this.powerLimiterConfigList;
|
|
return this.isEnabled() && cfg.metadata.battery_enabled && !cfg.is_inverter_solar_powered;
|
|
},
|
|
canUseVoltageThresholds() {
|
|
var 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() {
|
|
var cfg = this.powerLimiterConfigList;
|
|
|
|
var reset = function() {
|
|
cfg.inverter_channel_id = 0;
|
|
return false;
|
|
};
|
|
|
|
if (cfg.inverter_serial === '') { return reset(); }
|
|
|
|
if (cfg.is_inverter_solar_powered) { return reset(); }
|
|
|
|
var inverter = cfg.metadata.inverters[cfg.inverter_serial];
|
|
if (inverter === undefined) { return reset(); }
|
|
|
|
if (cfg.inverter_channel_id >= inverter.channels) {
|
|
reset();
|
|
}
|
|
|
|
return inverter.channels > 1;
|
|
},
|
|
getPowerLimiterConfig() {
|
|
this.dataLoading = true;
|
|
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, (key, value) => {
|
|
// do not submit metadata
|
|
if (key === "metadata") { return undefined; }
|
|
return value;
|
|
}));
|
|
|
|
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>
|