webapp: apply formatter on downstream sources
This commit is contained in:
parent
77af085ad3
commit
4334e60030
@ -1,124 +1,150 @@
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="'values' in batteryData"> <!-- suppress the card for MQTT battery provider -->
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': batteryData.data_age >= 20,
|
||||
'text-bg-primary': batteryData.data_age < 20,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.battery') }}: {{ batteryData.manufacturer }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;" v-if="'serial' in batteryData">
|
||||
{{ $t('home.SerialNumber') }}{{ batteryData.serial }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;" v-if="'fwversion' in batteryData">
|
||||
{{ $t('battery.FwVersion') }}: {{ batteryData.fwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;" v-if="'hwversion' in batteryData">
|
||||
{{ $t('battery.HwVersion') }}: {{ batteryData.hwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.DataAge') }} {{ $t('battery.Seconds', { 'val': batteryData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div v-for="(values, section) in batteryData.values" v-bind:key="section" class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header text-bg-info">{{ $t('battery.' + section) }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('battery.Value') }}</th>
|
||||
<th scope="col">{{ $t('battery.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in values" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td style="text-align: right">
|
||||
<template v-if="isStringValue(prop) && prop.translate">
|
||||
{{ $t('battery.' + prop.value) }}
|
||||
</template>
|
||||
<template v-else-if="isStringValue(prop)">
|
||||
{{ prop.value }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d})
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="!isStringValue(prop)">
|
||||
{{prop.u}}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card">
|
||||
<div :class="{'card-header': true, 'border-bottom-0': maxIssueValue === 0}">
|
||||
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||
{{ $t('battery.issues') }}
|
||||
<div v-if="maxIssueValue === 0" class="badge text-bg-success">{{ $t('battery.noIssues') }}</div>
|
||||
<div v-else-if="maxIssueValue === 1" class="badge text-bg-warning text-dark">{{ $t('battery.warning') }}</div>
|
||||
<div v-else-if="maxIssueValue === 2" class="badge text-bg-danger">{{ $t('battery.alarm') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" v-if="'issues' in batteryData">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.issueName') }}</th>
|
||||
<th scope="col">{{ $t('battery.issueType') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in batteryData.issues" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': prop === 1,
|
||||
'text-bg-danger': prop === 2
|
||||
}">
|
||||
<template v-if="prop === 1">{{ $t('battery.warning') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-else-if="'values' in batteryData">
|
||||
<!-- suppress the card for MQTT battery provider -->
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div
|
||||
class="card-header d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'text-bg-danger': batteryData.data_age >= 20,
|
||||
'text-bg-primary': batteryData.data_age < 20,
|
||||
}"
|
||||
>
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em">
|
||||
{{ $t('battery.battery') }}: {{ batteryData.manufacturer }}
|
||||
</div>
|
||||
<div style="padding-right: 2em" v-if="'serial' in batteryData">
|
||||
{{ $t('home.SerialNumber') }}{{ batteryData.serial }}
|
||||
</div>
|
||||
<div style="padding-right: 2em" v-if="'fwversion' in batteryData">
|
||||
{{ $t('battery.FwVersion') }}: {{ batteryData.fwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em" v-if="'hwversion' in batteryData">
|
||||
{{ $t('battery.HwVersion') }}: {{ batteryData.hwversion }}
|
||||
</div>
|
||||
<div style="padding-right: 2em">
|
||||
{{ $t('battery.DataAge') }}
|
||||
{{ $t('battery.Seconds', { val: batteryData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div
|
||||
v-for="(values, section) in batteryData.values"
|
||||
v-bind:key="section"
|
||||
class="col order-0"
|
||||
>
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header text-bg-info">{{ $t('battery.' + section) }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">
|
||||
{{ $t('battery.Value') }}
|
||||
</th>
|
||||
<th scope="col">{{ $t('battery.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in values" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td style="text-align: right">
|
||||
<template v-if="isStringValue(prop) && prop.translate">
|
||||
{{ $t('battery.' + prop.value) }}
|
||||
</template>
|
||||
<template v-else-if="isStringValue(prop)">
|
||||
{{ prop.value }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
$n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="!isStringValue(prop)">
|
||||
{{ prop.u }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card">
|
||||
<div :class="{ 'card-header': true, 'border-bottom-0': maxIssueValue === 0 }">
|
||||
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||
{{ $t('battery.issues') }}
|
||||
<div v-if="maxIssueValue === 0" class="badge text-bg-success">
|
||||
{{ $t('battery.noIssues') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="maxIssueValue === 1"
|
||||
class="badge text-bg-warning text-dark"
|
||||
>
|
||||
{{ $t('battery.warning') }}
|
||||
</div>
|
||||
<div v-else-if="maxIssueValue === 2" class="badge text-bg-danger">
|
||||
{{ $t('battery.alarm') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" v-if="'issues' in batteryData">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.issueName') }}</th>
|
||||
<th scope="col">{{ $t('battery.issueType') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in batteryData.issues" v-bind:key="key">
|
||||
<th scope="row">{{ $t('battery.' + key) }}</th>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
:class="{
|
||||
'text-bg-warning text-dark': prop === 1,
|
||||
'text-bg-danger': prop === 2,
|
||||
}"
|
||||
>
|
||||
<template v-if="prop === 1">{{
|
||||
$t('battery.warning')
|
||||
}}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -128,103 +154,101 @@ import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
batteryData: {} as Battery,
|
||||
isFirstFetchAfterConnect: true,
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
batteryData: {} as Battery,
|
||||
isFirstFetchAfterConnect: true,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
isStringValue(value: ValueObject | StringValue) : value is StringValue {
|
||||
return value && typeof value === 'object' && 'translate' in value;
|
||||
alertMessageLimit: '',
|
||||
alertTypeLimit: 'info',
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Battery");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/batterylivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.batteryData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Battery WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/batterylivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.batteryData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Battery websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.batteryData) {
|
||||
this.batteryData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
methods: {
|
||||
isStringValue(value: ValueObject | StringValue): value is StringValue {
|
||||
return value && typeof value === 'object' && 'translate' in value;
|
||||
},
|
||||
getInitialData() {
|
||||
console.log('Get initalData for Battery');
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch('/api/batterylivedata/status', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.batteryData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log('Starting connection to Battery WebSocket Server');
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === 'https:' ? 'wss' : 'ws'}://${authString}${host}/batterylivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.batteryData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log('Successfully connected to the Battery websocket server...');
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.batteryData) {
|
||||
this.batteryData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send('ping');
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
computed: {
|
||||
maxIssueValue() {
|
||||
return 'issues' in this.batteryData ? Math.max(...Object.values(this.batteryData.issues)) : 0;
|
||||
},
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
maxIssueValue() {
|
||||
return ('issues' in this.batteryData)?Math.max(...Object.values(this.batteryData.issues)):0;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<InputElement
|
||||
:label="$t('httprequestsettings.url')"
|
||||
v-model="cfg.url"
|
||||
type="text"
|
||||
maxlength="1024"
|
||||
placeholder="http://admin:supersecret@mypowermeter.home/status"
|
||||
prefix="GET "
|
||||
:tooltip="$t('httprequestsettings.urlDescription')"
|
||||
wide />
|
||||
:label="$t('httprequestsettings.url')"
|
||||
v-model="cfg.url"
|
||||
type="text"
|
||||
maxlength="1024"
|
||||
placeholder="http://admin:supersecret@mypowermeter.home/status"
|
||||
prefix="GET "
|
||||
:tooltip="$t('httprequestsettings.urlDescription')"
|
||||
wide
|
||||
/>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="auth_type" class="col-sm-4 col-form-label">{{ $t('httprequestsettings.authorization') }}</label>
|
||||
@ -22,42 +23,47 @@
|
||||
</div>
|
||||
|
||||
<InputElement
|
||||
v-if="cfg.auth_type != 0"
|
||||
:label="$t('httprequestsettings.username')"
|
||||
v-model="cfg.username"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
wide />
|
||||
v-if="cfg.auth_type != 0"
|
||||
:label="$t('httprequestsettings.username')"
|
||||
v-model="cfg.username"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
v-if="cfg.auth_type != 0"
|
||||
:label="$t('httprequestsettings.password')"
|
||||
v-model="cfg.password"
|
||||
type="password"
|
||||
maxlength="64"
|
||||
wide />
|
||||
v-if="cfg.auth_type != 0"
|
||||
:label="$t('httprequestsettings.password')"
|
||||
v-model="cfg.password"
|
||||
type="password"
|
||||
maxlength="64"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
:label="$t('httprequestsettings.headerKey')"
|
||||
v-model="cfg.header_key"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
:tooltip="$t('httprequestsettings.headerKeyDescription')"
|
||||
wide />
|
||||
:label="$t('httprequestsettings.headerKey')"
|
||||
v-model="cfg.header_key"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
:tooltip="$t('httprequestsettings.headerKeyDescription')"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
:label="$t('httprequestsettings.headerValue')"
|
||||
v-model="cfg.header_value"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
wide />
|
||||
:label="$t('httprequestsettings.headerValue')"
|
||||
v-model="cfg.header_value"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
:label="$t('httprequestsettings.timeout')"
|
||||
v-model="cfg.timeout"
|
||||
type="number"
|
||||
:postfix="$t('httprequestsettings.milliSeconds')"
|
||||
wide />
|
||||
:label="$t('httprequestsettings.timeout')"
|
||||
v-model="cfg.timeout"
|
||||
type="number"
|
||||
:postfix="$t('httprequestsettings.milliSeconds')"
|
||||
wide
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -67,28 +73,28 @@ import type { HttpRequestConfig } from '@/types/HttpRequestConfig';
|
||||
import InputElement from '@/components/InputElement.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: { 'modelValue': Object as () => HttpRequestConfig },
|
||||
props: { modelValue: Object as () => HttpRequestConfig },
|
||||
computed: {
|
||||
cfg: {
|
||||
get(): HttpRequestConfig {
|
||||
return this.modelValue || {} as HttpRequestConfig;
|
||||
return this.modelValue || ({} as HttpRequestConfig);
|
||||
},
|
||||
set(newValue: HttpRequestConfig): void {
|
||||
this.$emit('update:modelValue', newValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
InputElement
|
||||
InputElement,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
authTypeList: [
|
||||
{ key: 0, value: "None" },
|
||||
{ key: 1, value: "Basic" },
|
||||
{ key: 2, value: "Digest" },
|
||||
]
|
||||
{ key: 0, value: 'None' },
|
||||
{ key: 1, value: 'Basic' },
|
||||
{ key: 2, value: 'Digest' },
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,205 +1,270 @@
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': huaweiData.data_age > 20,
|
||||
'text-bg-primary': huaweiData.data_age < 19,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
Huawei R4850G2
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('huawei.DataAge') }} {{ $t('huawei.Seconds', { 'val': huaweiData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button :disabled="false" type="button" class="btn btn-sm btn-danger" @click="onShowLimitSettings()"
|
||||
v-tooltip :title="$t('huawei.ShowSetLimit')">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Input') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_voltage') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_voltage.v) }}</td>
|
||||
<td>{{ huaweiData.input_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_current') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_current.v) }}</td>
|
||||
<td>{{ huaweiData.input_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_power') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_power.v) }}</td>
|
||||
<td>{{ huaweiData.input_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.input_temp.v) }}</td>
|
||||
<td>{{ huaweiData.input_temp.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.efficiency') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.efficiency.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.efficiency.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Output') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_voltage') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_voltage.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_current.v.toFixed(2) }}</td>
|
||||
<td>{{ huaweiData.output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.max_output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.max_output_current.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.max_output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_power') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_power.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.output_temp.v) }}</td>
|
||||
<td>{{ huaweiData.output_temp.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="huaweiLimitSettingView" ref="huaweiLimitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ $t('huawei.LimitSettings') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<template v-else>
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div
|
||||
class="card-header d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'text-bg-danger': huaweiData.data_age > 20,
|
||||
'text-bg-primary': huaweiData.data_age < 19,
|
||||
}"
|
||||
>
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em">Huawei R4850G2</div>
|
||||
<div style="padding-right: 2em">
|
||||
{{ $t('huawei.DataAge') }} {{ $t('huawei.Seconds', { val: huaweiData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button
|
||||
:disabled="false"
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="onShowLimitSettings()"
|
||||
v-tooltip
|
||||
:title="$t('huawei.ShowSetLimit')"
|
||||
>
|
||||
<BIconSpeedometer style="font-size: 24px" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Input') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">
|
||||
{{ $t('huawei.Value') }}
|
||||
</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_voltage') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumber(huaweiData.input_voltage.v) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.input_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_current') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumber(huaweiData.input_current.v) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.input_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_power') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ formatNumber(huaweiData.input_power.v) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.input_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_temp') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ Math.round(huaweiData.input_temp.v) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.input_temp.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.efficiency') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ huaweiData.efficiency.v.toFixed(1) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.efficiency.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Output') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">
|
||||
{{ $t('huawei.Value') }}
|
||||
</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_voltage') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ huaweiData.output_voltage.v.toFixed(1) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.output_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_current') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ huaweiData.output_current.v.toFixed(2) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.max_output_current') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ huaweiData.max_output_current.v.toFixed(1) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.max_output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_power') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ huaweiData.output_power.v.toFixed(1) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.output_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_temp') }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ Math.round(huaweiData.output_temp.v) }}
|
||||
</td>
|
||||
<td>{{ huaweiData.output_temp.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">{{ $t('huawei.CurrentLimit') }} </label>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputVoltageTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetVoltageLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchVoltage"
|
||||
v-model="targetLimitList.voltage_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" step="0.01" name="inputVoltageTargetLimit" class="form-control" id="inputVoltageTargetLimit"
|
||||
:min="targetVoltageLimitMin" :max="targetVoltageLimitMax" v-model="targetLimitList.voltage"
|
||||
:disabled=!targetLimitList.voltage_valid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9">
|
||||
<div v-if="targetLimitList.voltage < targetVoltageLimitMinOffline" class="alert alert-secondary mt-3"
|
||||
role="alert" v-html="$t('huawei.LimitHint')"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputCurrentTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetCurrentLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCurrentt"
|
||||
v-model="targetLimitList.current_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" step="0.1" name="inputCurrentTargetLimit" class="form-control" id="inputCurrentTargetLimit"
|
||||
:min="targetCurrentLimitMin" :max="targetCurrentLimitMax" v-model="targetLimitList.current"
|
||||
:disabled=!targetLimitList.current_valid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">{{ $t('huawei.SetOnline')
|
||||
}}</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">{{
|
||||
$t('huawei.SetOffline')
|
||||
}}</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t('huawei.Close') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="modal" id="huaweiLimitSettingView" ref="huaweiLimitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ $t('huawei.LimitSettings') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label"
|
||||
>{{ $t('huawei.CurrentLimit') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputVoltageTargetLimit" class="col-sm-3 col-form-label">{{
|
||||
$t('huawei.SetVoltageLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchVoltage"
|
||||
v-model="targetLimitList.voltage_valid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="inputVoltageTargetLimit"
|
||||
class="form-control"
|
||||
id="inputVoltageTargetLimit"
|
||||
:min="targetVoltageLimitMin"
|
||||
:max="targetVoltageLimitMax"
|
||||
v-model="targetLimitList.voltage"
|
||||
:disabled="!targetLimitList.voltage_valid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9">
|
||||
<div
|
||||
v-if="targetLimitList.voltage < targetVoltageLimitMinOffline"
|
||||
class="alert alert-secondary mt-3"
|
||||
role="alert"
|
||||
v-html="$t('huawei.LimitHint')"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputCurrentTargetLimit" class="col-sm-3 col-form-label">{{
|
||||
$t('huawei.SetCurrentLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="flexSwitchCurrentt"
|
||||
v-model="targetLimitList.current_valid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
name="inputCurrentTargetLimit"
|
||||
class="form-control"
|
||||
id="inputCurrentTargetLimit"
|
||||
:min="targetCurrentLimitMin"
|
||||
:max="targetCurrentLimitMax"
|
||||
v-model="targetLimitList.current"
|
||||
:disabled="!targetLimitList.current_valid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">
|
||||
{{ $t('huawei.SetOnline') }}
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">
|
||||
{{ $t('huawei.SetOffline') }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t('huawei.Close') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -209,150 +274,143 @@ import type { HuaweiLimitConfig } from '@/types/HuaweiLimitConfig';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import {
|
||||
BIconSpeedometer,
|
||||
} from 'bootstrap-icons-vue';
|
||||
import { BIconSpeedometer } from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BIconSpeedometer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
huaweiData: {} as Huawei,
|
||||
isFirstFetchAfterConnect: true,
|
||||
targetVoltageLimitMin: 42,
|
||||
targetVoltageLimitMinOffline: 48,
|
||||
targetVoltageLimitMax: 58,
|
||||
targetCurrentLimitMin: 0,
|
||||
targetCurrentLimitMax: 60,
|
||||
targetLimitList: {} as HuaweiLimitConfig,
|
||||
targetLimitPersistent: false,
|
||||
huaweiLimitSettingView: {} as bootstrap.Modal,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Huawei");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/huaweilivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.huaweiData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
components: {
|
||||
BIconSpeedometer,
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Huawei WebSocket Server");
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
huaweiData: {} as Huawei,
|
||||
isFirstFetchAfterConnect: true,
|
||||
targetVoltageLimitMin: 42,
|
||||
targetVoltageLimitMinOffline: 48,
|
||||
targetVoltageLimitMax: 58,
|
||||
targetCurrentLimitMin: 0,
|
||||
targetCurrentLimitMax: 60,
|
||||
targetLimitList: {} as HuaweiLimitConfig,
|
||||
targetLimitPersistent: false,
|
||||
huaweiLimitSettingView: {} as bootstrap.Modal,
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/huaweilivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.huaweiData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Huawei websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
alertMessageLimit: '',
|
||||
alertTypeLimit: 'info',
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.huaweiData) {
|
||||
this.huaweiData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
formatNumber(num: number) {
|
||||
return new Intl.NumberFormat(
|
||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||
).format(num);
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings() {
|
||||
this.huaweiLimitSettingView = new bootstrap.Modal('#huaweiLimitSettingView');
|
||||
this.huaweiLimitSettingView.show();
|
||||
},
|
||||
onSetLimitSettings(online: boolean) {
|
||||
this.targetLimitList.online = online;
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log('Get initalData for Huawei');
|
||||
this.dataLoading = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.targetLimitList));
|
||||
fetch('/api/huaweilivedata/status', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.huaweiData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log('Starting connection to Huawei WebSocket Server');
|
||||
|
||||
console.log(this.targetLimitList);
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === 'https:' ? 'wss' : 'ws'}://${authString}${host}/huaweilivedata`;
|
||||
|
||||
fetch("/api/huawei/limit/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.huaweiLimitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.huaweiData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log('Successfully connected to the Huawei websocket server...');
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.huaweiData) {
|
||||
this.huaweiData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send('ping');
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
formatNumber(num: number) {
|
||||
return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(num);
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings() {
|
||||
this.huaweiLimitSettingView = new bootstrap.Modal('#huaweiLimitSettingView');
|
||||
this.huaweiLimitSettingView.show();
|
||||
},
|
||||
onSetLimitSettings(online: boolean) {
|
||||
this.targetLimitList.online = online;
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify(this.targetLimitList));
|
||||
|
||||
console.log(this.targetLimitList);
|
||||
|
||||
fetch('/api/huawei/limit/config', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((response) => {
|
||||
if (response.type == 'success') {
|
||||
this.huaweiLimitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
<template>
|
||||
<div class="row row-cols-1 row-cols-md-3 g-3">
|
||||
<div class="col" v-if="totalVeData.enabled">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.MpptTotalYieldTotal')">
|
||||
<CardElement
|
||||
centerContent
|
||||
textVariant="text-bg-success"
|
||||
:text="$t('invertertotalinfo.MpptTotalYieldTotal')"
|
||||
>
|
||||
<h2>
|
||||
{{ $n(totalVeData.total.YieldTotal.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.YieldTotal.d,
|
||||
maximumFractionDigits:totalVeData.total.YieldTotal.d
|
||||
}) }}
|
||||
{{
|
||||
$n(totalVeData.total.YieldTotal.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.YieldTotal.d,
|
||||
maximumFractionDigits: totalVeData.total.YieldTotal.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ totalVeData.total.YieldTotal.u }}</small>
|
||||
</h2>
|
||||
</CardElement>
|
||||
@ -14,10 +20,12 @@
|
||||
<div class="col" v-if="totalVeData.enabled">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.MpptTotalYieldDay')">
|
||||
<h2>
|
||||
{{ $n(totalVeData.total.YieldDay.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.YieldDay.d,
|
||||
maximumFractionDigits: totalVeData.total.YieldDay.d
|
||||
}) }}
|
||||
{{
|
||||
$n(totalVeData.total.YieldDay.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.YieldDay.d,
|
||||
maximumFractionDigits: totalVeData.total.YieldDay.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ totalVeData.total.YieldDay.u }}</small>
|
||||
</h2>
|
||||
</CardElement>
|
||||
@ -25,16 +33,22 @@
|
||||
<div class="col" v-if="totalVeData.enabled">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.MpptTotalPower')">
|
||||
<h2>
|
||||
{{ $n(totalVeData.total.Power.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.Power.d,
|
||||
maximumFractionDigits: totalVeData.total.Power.d
|
||||
}) }}
|
||||
{{
|
||||
$n(totalVeData.total.Power.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.Power.d,
|
||||
maximumFractionDigits: totalVeData.total.Power.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ totalVeData.total.Power.u }}</small>
|
||||
</h2>
|
||||
</CardElement>
|
||||
</div>
|
||||
<div class="col">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.InverterTotalYieldTotal')">
|
||||
<CardElement
|
||||
centerContent
|
||||
textVariant="text-bg-success"
|
||||
:text="$t('invertertotalinfo.InverterTotalYieldTotal')"
|
||||
>
|
||||
<h2>
|
||||
{{
|
||||
$n(totalData.YieldTotal.v, 'decimal', {
|
||||
@ -47,7 +61,11 @@
|
||||
</CardElement>
|
||||
</div>
|
||||
<div class="col">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.InverterTotalYieldDay')">
|
||||
<CardElement
|
||||
centerContent
|
||||
textVariant="text-bg-success"
|
||||
:text="$t('invertertotalinfo.InverterTotalYieldDay')"
|
||||
>
|
||||
<h2>
|
||||
{{
|
||||
$n(totalData.YieldDay.v, 'decimal', {
|
||||
@ -74,46 +92,64 @@
|
||||
</div>
|
||||
<template v-if="totalBattData.enabled">
|
||||
<div class="col">
|
||||
<CardElement centerContent flexChildren textVariant="text-bg-success" :text="$t('invertertotalinfo.BatteryCharge')">
|
||||
<CardElement
|
||||
centerContent
|
||||
flexChildren
|
||||
textVariant="text-bg-success"
|
||||
:text="$t('invertertotalinfo.BatteryCharge')"
|
||||
>
|
||||
<div class="flex-fill" v-if="totalBattData.soc">
|
||||
<h2>
|
||||
{{ $n(totalBattData.soc.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.soc.d,
|
||||
maximumFractionDigits: totalBattData.soc.d
|
||||
}) }}
|
||||
{{
|
||||
$n(totalBattData.soc.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.soc.d,
|
||||
maximumFractionDigits: totalBattData.soc.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ totalBattData.soc.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" v-if="totalBattData.voltage">
|
||||
<h2>
|
||||
{{ $n(totalBattData.voltage.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.voltage.d,
|
||||
maximumFractionDigits: totalBattData.voltage.d
|
||||
}) }}
|
||||
{{
|
||||
$n(totalBattData.voltage.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.voltage.d,
|
||||
maximumFractionDigits: totalBattData.voltage.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ totalBattData.voltage.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</CardElement>
|
||||
</div>
|
||||
<div class="col" v-if="totalBattData.power || totalBattData.current">
|
||||
<CardElement centerContent flexChildren textVariant="text-bg-success" :text="$t('invertertotalinfo.BatteryPower')">
|
||||
<CardElement
|
||||
centerContent
|
||||
flexChildren
|
||||
textVariant="text-bg-success"
|
||||
:text="$t('invertertotalinfo.BatteryPower')"
|
||||
>
|
||||
<div class="flex-fill" v-if="totalBattData.power">
|
||||
<h2>
|
||||
{{ $n(totalBattData.power.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.power.d,
|
||||
maximumFractionDigits: totalBattData.power.d
|
||||
}) }}
|
||||
{{
|
||||
$n(totalBattData.power.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.power.d,
|
||||
maximumFractionDigits: totalBattData.power.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ totalBattData.power.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" v-if="totalBattData.current">
|
||||
<h2>
|
||||
{{ $n(totalBattData.current.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.current.d,
|
||||
maximumFractionDigits: totalBattData.current.d
|
||||
}) }}
|
||||
{{
|
||||
$n(totalBattData.current.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.current.d,
|
||||
maximumFractionDigits: totalBattData.current.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ totalBattData.current.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
@ -123,22 +159,26 @@
|
||||
<div class="col" v-if="powerMeterData.enabled">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HomePower')">
|
||||
<h2>
|
||||
{{ $n(powerMeterData.Power.v, 'decimal', {
|
||||
minimumFractionDigits: powerMeterData.Power.d,
|
||||
maximumFractionDigits: powerMeterData.Power.d
|
||||
}) }}
|
||||
<small class="text-muted">{{powerMeterData.Power.u }}</small>
|
||||
{{
|
||||
$n(powerMeterData.Power.v, 'decimal', {
|
||||
minimumFractionDigits: powerMeterData.Power.d,
|
||||
maximumFractionDigits: powerMeterData.Power.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ powerMeterData.Power.u }}</small>
|
||||
</h2>
|
||||
</CardElement>
|
||||
</div>
|
||||
<div class="col" v-if="huaweiData.enabled">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HuaweiPower')">
|
||||
<h2>
|
||||
{{ $n(huaweiData.Power.v, 'decimal', {
|
||||
minimumFractionDigits: huaweiData.Power.d,
|
||||
maximumFractionDigits: huaweiData.Power.d
|
||||
}) }}
|
||||
<small class="text-muted">{{huaweiData.Power.u }}</small>
|
||||
{{
|
||||
$n(huaweiData.Power.v, 'decimal', {
|
||||
minimumFractionDigits: huaweiData.Power.d,
|
||||
maximumFractionDigits: huaweiData.Power.d,
|
||||
})
|
||||
}}
|
||||
<small class="text-muted">{{ huaweiData.Power.u }}</small>
|
||||
</h2>
|
||||
</CardElement>
|
||||
</div>
|
||||
@ -154,12 +194,12 @@ export default defineComponent({
|
||||
components: {
|
||||
CardElement,
|
||||
},
|
||||
props: {
|
||||
totalData: { type: Object as PropType<Total>, required: true },
|
||||
totalVeData: { type: Object as PropType<Vedirect>, required: true },
|
||||
totalBattData: { type: Object as PropType<Battery>, required: true },
|
||||
powerMeterData: { type: Object as PropType<PowerMeter>, required: true },
|
||||
huaweiData: { type: Object as PropType<Huawei>, required: true },
|
||||
},
|
||||
props: {
|
||||
totalData: { type: Object as PropType<Total>, required: true },
|
||||
totalVeData: { type: Object as PropType<Vedirect>, required: true },
|
||||
totalBattData: { type: Object as PropType<Battery>, required: true },
|
||||
powerMeterData: { type: Object as PropType<PowerMeter>, required: true },
|
||||
huaweiData: { type: Object as PropType<Huawei>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
<BIconSun v-else width="30" height="30" class="d-inline-block align-text-top text-warning" />
|
||||
|
||||
<span style="margin-left: 0.5rem"> OpenDTU-OnBattery </span>
|
||||
<span class="text-info mx-2"><BIconBatteryCharging width="20" height="20" class="d-inline-block align-text-center" /></span>
|
||||
<span class="text-info mx-2"
|
||||
><BIconBatteryCharging width="20" height="20" class="d-inline-block align-text-center"
|
||||
/></span>
|
||||
</router-link>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
@ -69,20 +71,30 @@
|
||||
$t('menu.DTUSettings')
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{ $t('menu.VedirectSettings') }}</router-link>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{
|
||||
$t('menu.VedirectSettings')
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/powermeter">{{ $t('menu.PowerMeterSettings') }}</router-link>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/powermeter">{{
|
||||
$t('menu.PowerMeterSettings')
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/powerlimiter">Dynamic Power Limiter</router-link>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/powerlimiter"
|
||||
>Dynamic Power Limiter</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/battery">{{ $t('menu.BatterySettings') }}</router-link>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/battery">{{
|
||||
$t('menu.BatterySettings')
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/chargerac">{{ $t('menu.AcChargerSettings') }}</router-link>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/chargerac">{{
|
||||
$t('menu.AcChargerSettings')
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/device">{{
|
||||
@ -142,7 +154,9 @@
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/info/vedirect">{{ $t('menu.Vedirect') }}</router-link>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/info/vedirect">{{
|
||||
$t('menu.Vedirect')
|
||||
}}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider" />
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
<template>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
@ -10,44 +9,53 @@
|
||||
<div class="row gy-3 mt-0" v-for="(item, serial) in vedirect.instances" :key="serial">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center"
|
||||
<div
|
||||
class="card-header d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'text-bg-danger': item.data_age_ms >= 10000,
|
||||
'text-bg-primary': item.data_age_ms < 10000,
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
<div style="padding-right: 2em">
|
||||
{{ item.product_id }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
<div style="padding-right: 2em">
|
||||
{{ $t('vedirecthome.SerialNumber') }}: {{ serial }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('vedirecthome.FirmwareVersion') }}: {{ item.firmware_version }}
|
||||
<div style="padding-right: 2em">
|
||||
{{ $t('vedirecthome.FirmwareVersion') }}: {{ item.firmware_version }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('vedirecthome.DataAge') }}: {{ $t('vedirecthome.Seconds', {'val': Math.floor(item.data_age_ms / 1000)}) }}
|
||||
<div style="padding-right: 2em">
|
||||
{{ $t('vedirecthome.DataAge') }}:
|
||||
{{ $t('vedirecthome.Seconds', { val: Math.floor(item.data_age_ms / 1000) }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button"
|
||||
class="btn btn-sm" v-tooltip :title="$t('vedirecthome.PowerLimiterState')">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
v-tooltip
|
||||
:title="$t('vedirecthome.PowerLimiterState')"
|
||||
>
|
||||
<div v-if="dplData.PLSTATE == 0">
|
||||
<BIconXCircleFill style="font-size:24px;" />
|
||||
<BIconXCircleFill style="font-size: 24px" />
|
||||
</div>
|
||||
<div v-else-if="dplData.PLSTATE == 1">
|
||||
<BIconBatteryCharging style="font-size:24px;" />
|
||||
<BIconBatteryCharging style="font-size: 24px" />
|
||||
</div>
|
||||
<div v-else-if="dplData.PLSTATE == 2">
|
||||
<BIconSun style="font-size:24px;" />
|
||||
<BIconSun style="font-size: 24px" />
|
||||
</div>
|
||||
<div v-else-if="dplData.PLSTATE == 3">
|
||||
<BIconBatteryHalf style="font-size:24px;" />
|
||||
<BIconBatteryHalf style="font-size: 24px" />
|
||||
</div>
|
||||
<span v-if="dplData.PLSTATE != -1"
|
||||
class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-info">
|
||||
<span
|
||||
v-if="dplData.PLSTATE != -1"
|
||||
class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-info"
|
||||
>
|
||||
{{ dplData.PLLIMIT }} W
|
||||
</span>
|
||||
</button>
|
||||
@ -56,34 +64,42 @@
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div v-for="(values, section) in item.values" v-bind:key="section" class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': (section === 'device') }">
|
||||
<div :class="(section === 'device')?'card-header text-bg-info':'card-header'">{{ $t('vedirecthome.section_' + section) }}</div>
|
||||
<div class="card" :class="{ 'border-info': section === 'device' }">
|
||||
<div :class="section === 'device' ? 'card-header text-bg-info' : 'card-header'">
|
||||
{{ $t('vedirecthome.section_' + section) }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('vedirecthome.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('vedirecthome.Value') }}</th>
|
||||
<th style="text-align: right" scope="col">
|
||||
{{ $t('vedirecthome.Value') }}
|
||||
</th>
|
||||
<th scope="col">{{ $t('vedirecthome.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in values" v-bind:key="key">
|
||||
<th scope="row">{{ $t('vedirecthome.' + section + '.' + key) }}</th>
|
||||
<th scope="row">
|
||||
{{ $t('vedirecthome.' + section + '.' + key) }}
|
||||
</th>
|
||||
<td style="text-align: right">
|
||||
<template v-if="typeof prop === 'string'">
|
||||
{{ prop }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d})
|
||||
{{
|
||||
$n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
<td v-if="typeof prop === 'string'"></td>
|
||||
<td v-else>{{prop.u}}</td>
|
||||
<td v-else>{{ prop.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -97,29 +113,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { DynamicPowerLimiter, Vedirect } from '@/types/VedirectLiveDataStatus';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
import {
|
||||
BIconSun,
|
||||
BIconBatteryCharging,
|
||||
BIconBatteryHalf,
|
||||
BIconXCircleFill
|
||||
} from 'bootstrap-icons-vue';
|
||||
|
||||
import { BIconSun, BIconBatteryCharging, BIconBatteryHalf, BIconXCircleFill } from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BIconSun,
|
||||
BIconBatteryCharging,
|
||||
BIconBatteryHalf,
|
||||
BIconXCircleFill
|
||||
BIconXCircleFill,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -141,44 +148,43 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for VeDirect");
|
||||
console.log('Get initalData for VeDirect');
|
||||
this.dataLoading = true;
|
||||
fetch("/api/vedirectlivedata/status", { headers: authHeader() })
|
||||
fetch('/api/vedirectlivedata/status', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((root) => {
|
||||
this.dplData = root["dpl"];
|
||||
this.vedirect = root["vedirect"];
|
||||
this.dplData = root['dpl'];
|
||||
this.vedirect = root['vedirect'];
|
||||
this.dataLoading = false;
|
||||
this.resetDataAging(Object.keys(root["vedirect"]["instances"]));
|
||||
this.resetDataAging(Object.keys(root['vedirect']['instances']));
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to VeDirect WebSocket Server");
|
||||
console.log('Starting connection to VeDirect WebSocket Server');
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/vedirectlivedata`;
|
||||
const webSocketUrl = `${protocol === 'https:' ? 'wss' : 'ws'}://${authString}${host}/vedirectlivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
const root = JSON.parse(event.data);
|
||||
this.dplData = root["dpl"];
|
||||
if (root["vedirect"]["full_update"] === true) {
|
||||
this.vedirect = root["vedirect"];
|
||||
this.dplData = root['dpl'];
|
||||
if (root['vedirect']['full_update'] === true) {
|
||||
this.vedirect = root['vedirect'];
|
||||
} else {
|
||||
Object.assign(this.vedirect.instances, root["vedirect"]["instances"]);
|
||||
Object.assign(this.vedirect.instances, root['vedirect']['instances']);
|
||||
}
|
||||
this.resetDataAging(Object.keys(root["vedirect"]["instances"]));
|
||||
this.resetDataAging(Object.keys(root['vedirect']['instances']));
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the VeDirect websocket server...");
|
||||
console.log('Successfully connected to the VeDirect websocket server...');
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
@ -199,7 +205,9 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
doDataAging(serial: string) {
|
||||
if (this.vedirect?.instances?.[serial] === undefined) { return; }
|
||||
if (this.vedirect?.instances?.[serial] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.vedirect.instances[serial].data_age_ms += 1000;
|
||||
|
||||
@ -213,7 +221,7 @@ export default defineComponent({
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
this.socket.send('ping');
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
|
||||
@ -366,7 +366,7 @@
|
||||
"Disconnected": "getrennt"
|
||||
},
|
||||
"vedirectinfo": {
|
||||
"VedirectInformation" : "VE.Direct Info",
|
||||
"VedirectInformation": "VE.Direct Info",
|
||||
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
|
||||
"Status": "@:ntpinfo.Status",
|
||||
"Enabled": "@:mqttinfo.Enabled",
|
||||
@ -553,7 +553,7 @@
|
||||
"VerboseLogging": "@:base.VerboseLogging",
|
||||
"UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden"
|
||||
},
|
||||
"powermeteradmin":{
|
||||
"powermeteradmin": {
|
||||
"PowerMeterSettings": "Stromzähler Einstellungen",
|
||||
"PowerMeterConfiguration": "Stromzähler Konfiguration",
|
||||
"PowerMeterEnable": "Aktiviere Stromzähler",
|
||||
|
||||
@ -368,7 +368,7 @@
|
||||
"Disconnected": "disconnected"
|
||||
},
|
||||
"vedirectinfo": {
|
||||
"VedirectInformation" : "VE.Direct Info",
|
||||
"VedirectInformation": "VE.Direct Info",
|
||||
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
|
||||
"Status": "@:ntpinfo.Status",
|
||||
"Enabled": "@:mqttinfo.Enabled",
|
||||
@ -555,7 +555,7 @@
|
||||
"VerboseLogging": "@:base.VerboseLogging",
|
||||
"UpdatesOnly": "Publish values to MQTT only when they change"
|
||||
},
|
||||
"powermeteradmin":{
|
||||
"powermeteradmin": {
|
||||
"PowerMeterSettings": "Power Meter Settings",
|
||||
"PowerMeterConfiguration": "Power Meter Configuration",
|
||||
"PowerMeterEnable": "Enable Power Meter",
|
||||
@ -847,7 +847,7 @@
|
||||
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
|
||||
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
|
||||
"format_unknown": "Unknown format"
|
||||
},
|
||||
},
|
||||
"huawei": {
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": " {val} seconds",
|
||||
|
||||
@ -401,7 +401,7 @@
|
||||
"Disconnected": "déconnecté"
|
||||
},
|
||||
"vedirectinfo": {
|
||||
"VedirectInformation" : "VE.Direct Info",
|
||||
"VedirectInformation": "VE.Direct Info",
|
||||
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
|
||||
"Status": "@:ntpinfo.Status",
|
||||
"Enabled": "@:mqttinfo.Enabled",
|
||||
@ -681,54 +681,54 @@
|
||||
"Cancel": "@:base.Cancel"
|
||||
},
|
||||
"powerlimiteradmin": {
|
||||
"PowerLimiterSettings": "Dynamic Power Limiter Settings",
|
||||
"ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.",
|
||||
"ConfigHints": "Configuration Notes",
|
||||
"ConfigHintRequirement": "Required",
|
||||
"ConfigHintOptional": "Optional",
|
||||
"ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:",
|
||||
"ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).",
|
||||
"ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.",
|
||||
"ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.",
|
||||
"ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.",
|
||||
"ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.",
|
||||
"General": "General",
|
||||
"Enable": "Enable",
|
||||
"VerboseLogging": "@:base.VerboseLogging",
|
||||
"SolarPassthrough": "Solar-Passthrough",
|
||||
"EnableSolarPassthrough": "Enable Solar-Passthrough",
|
||||
"SolarPassthroughLosses": "(Full) Solar-Passthrough Losses",
|
||||
"SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.",
|
||||
"BatteryDischargeAtNight": "Use battery at night even if only partially charged",
|
||||
"SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.",
|
||||
"InverterSettings": "Inverter",
|
||||
"Inverter": "Target Inverter",
|
||||
"SelectInverter": "Select an inverter...",
|
||||
"InverterChannelId": "Input used for voltage measurements",
|
||||
"TargetPowerConsumption": "Target Grid Consumption",
|
||||
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
|
||||
"TargetPowerConsumptionHysteresis": "Hysteresis",
|
||||
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.",
|
||||
"LowerPowerLimit": "Minimum Power Limit",
|
||||
"LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.",
|
||||
"BaseLoadLimit": "Base Load",
|
||||
"BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.",
|
||||
"UpperPowerLimit": "Maximum Power Limit",
|
||||
"UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.",
|
||||
"SocThresholds": "Battery State of Charge (SoC) Thresholds",
|
||||
"IgnoreSoc": "Ignore Battery SoC",
|
||||
"StartThreshold": "Start Threshold for Battery Discharging",
|
||||
"StopThreshold": "Stop Threshold for Battery Discharging",
|
||||
"FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold",
|
||||
"FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.",
|
||||
"VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold",
|
||||
"VoltageLoadCorrectionFactor": "Load correction factor",
|
||||
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.",
|
||||
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
|
||||
"InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.",
|
||||
"InverterIsSolarPowered": "Inverter is powered by solar modules",
|
||||
"VoltageThresholds": "Battery Voltage Thresholds",
|
||||
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor)."
|
||||
"PowerLimiterSettings": "Dynamic Power Limiter Settings",
|
||||
"ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.",
|
||||
"ConfigHints": "Configuration Notes",
|
||||
"ConfigHintRequirement": "Required",
|
||||
"ConfigHintOptional": "Optional",
|
||||
"ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:",
|
||||
"ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).",
|
||||
"ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.",
|
||||
"ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.",
|
||||
"ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.",
|
||||
"ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.",
|
||||
"General": "General",
|
||||
"Enable": "Enable",
|
||||
"VerboseLogging": "@:base.VerboseLogging",
|
||||
"SolarPassthrough": "Solar-Passthrough",
|
||||
"EnableSolarPassthrough": "Enable Solar-Passthrough",
|
||||
"SolarPassthroughLosses": "(Full) Solar-Passthrough Losses",
|
||||
"SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.",
|
||||
"BatteryDischargeAtNight": "Use battery at night even if only partially charged",
|
||||
"SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.",
|
||||
"InverterSettings": "Inverter",
|
||||
"Inverter": "Target Inverter",
|
||||
"SelectInverter": "Select an inverter...",
|
||||
"InverterChannelId": "Input used for voltage measurements",
|
||||
"TargetPowerConsumption": "Target Grid Consumption",
|
||||
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
|
||||
"TargetPowerConsumptionHysteresis": "Hysteresis",
|
||||
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.",
|
||||
"LowerPowerLimit": "Minimum Power Limit",
|
||||
"LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.",
|
||||
"BaseLoadLimit": "Base Load",
|
||||
"BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.",
|
||||
"UpperPowerLimit": "Maximum Power Limit",
|
||||
"UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.",
|
||||
"SocThresholds": "Battery State of Charge (SoC) Thresholds",
|
||||
"IgnoreSoc": "Ignore Battery SoC",
|
||||
"StartThreshold": "Start Threshold for Battery Discharging",
|
||||
"StopThreshold": "Stop Threshold for Battery Discharging",
|
||||
"FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold",
|
||||
"FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.",
|
||||
"VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold",
|
||||
"VoltageLoadCorrectionFactor": "Load correction factor",
|
||||
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.",
|
||||
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
|
||||
"InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.",
|
||||
"InverterIsSolarPowered": "Inverter is powered by solar modules",
|
||||
"VoltageThresholds": "Battery Voltage Thresholds",
|
||||
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor)."
|
||||
},
|
||||
"login": {
|
||||
"Login": "Connexion",
|
||||
|
||||
@ -8,10 +8,10 @@ import DtuAdminView from '@/views/DtuAdminView.vue';
|
||||
import ErrorView from '@/views/ErrorView.vue';
|
||||
import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue';
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import VedirectAdminView from '@/views/VedirectAdminView.vue'
|
||||
import PowerMeterAdminView from '@/views/PowerMeterAdminView.vue'
|
||||
import PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue'
|
||||
import VedirectInfoView from '@/views/VedirectInfoView.vue'
|
||||
import VedirectAdminView from '@/views/VedirectAdminView.vue';
|
||||
import PowerMeterAdminView from '@/views/PowerMeterAdminView.vue';
|
||||
import PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue';
|
||||
import VedirectInfoView from '@/views/VedirectInfoView.vue';
|
||||
import InverterAdminView from '@/views/InverterAdminView.vue';
|
||||
import LoginView from '@/views/LoginView.vue';
|
||||
import MaintenanceRebootView from '@/views/MaintenanceRebootView.vue';
|
||||
@ -74,11 +74,11 @@ const router = createRouter({
|
||||
name: 'Web Console',
|
||||
component: ConsoleInfoView,
|
||||
},
|
||||
{
|
||||
path: '/info/vedirect',
|
||||
name: 'VE.Direct',
|
||||
component: VedirectInfoView
|
||||
},
|
||||
{
|
||||
path: '/info/vedirect',
|
||||
name: 'VE.Direct',
|
||||
component: VedirectInfoView,
|
||||
},
|
||||
{
|
||||
path: '/settings/network',
|
||||
name: 'Network Settings',
|
||||
@ -89,31 +89,31 @@ const router = createRouter({
|
||||
name: 'NTP Settings',
|
||||
component: NtpAdminView,
|
||||
},
|
||||
{
|
||||
path: '/settings/vedirect',
|
||||
name: 'VE.Direct Settings',
|
||||
component: VedirectAdminView
|
||||
},
|
||||
{
|
||||
path: '/settings/powermeter',
|
||||
name: 'Power meter Settings',
|
||||
component: PowerMeterAdminView
|
||||
},
|
||||
{
|
||||
path: '/settings/powerlimiter',
|
||||
name: 'Power limiter Settings',
|
||||
component: PowerLimiterAdminView
|
||||
},
|
||||
{
|
||||
path: '/settings/battery',
|
||||
name: 'Battery Settings',
|
||||
component: BatteryAdminView
|
||||
},
|
||||
{
|
||||
path: '/settings/chargerac',
|
||||
name: 'Charger Settings',
|
||||
component: AcChargerAdminView
|
||||
},
|
||||
{
|
||||
path: '/settings/vedirect',
|
||||
name: 'VE.Direct Settings',
|
||||
component: VedirectAdminView,
|
||||
},
|
||||
{
|
||||
path: '/settings/powermeter',
|
||||
name: 'Power meter Settings',
|
||||
component: PowerMeterAdminView,
|
||||
},
|
||||
{
|
||||
path: '/settings/powerlimiter',
|
||||
name: 'Power limiter Settings',
|
||||
component: PowerLimiterAdminView,
|
||||
},
|
||||
{
|
||||
path: '/settings/battery',
|
||||
name: 'Battery Settings',
|
||||
component: BatteryAdminView,
|
||||
},
|
||||
{
|
||||
path: '/settings/chargerac',
|
||||
name: 'Charger Settings',
|
||||
component: AcChargerAdminView,
|
||||
},
|
||||
{
|
||||
path: '/settings/mqtt',
|
||||
name: 'MqTT Settings',
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
export interface AcChargerConfig {
|
||||
enabled: boolean;
|
||||
verbose_logging: boolean;
|
||||
can_controller_frequency: number;
|
||||
auto_power_enabled: boolean;
|
||||
auto_power_batterysoc_limits_enabled: boolean;
|
||||
voltage_limit: number;
|
||||
enable_voltage_limit: number;
|
||||
lower_power_limit: number;
|
||||
upper_power_limit: number;
|
||||
emergency_charge_enabled: boolean;
|
||||
stop_batterysoc_threshold: number;
|
||||
target_power_consumption: number;
|
||||
enabled: boolean;
|
||||
verbose_logging: boolean;
|
||||
can_controller_frequency: number;
|
||||
auto_power_enabled: boolean;
|
||||
auto_power_batterysoc_limits_enabled: boolean;
|
||||
voltage_limit: number;
|
||||
enable_voltage_limit: number;
|
||||
lower_power_limit: number;
|
||||
upper_power_limit: number;
|
||||
emergency_charge_enabled: boolean;
|
||||
stop_batterysoc_threshold: number;
|
||||
target_power_consumption: number;
|
||||
}
|
||||
|
||||
@ -15,4 +15,4 @@ export interface Battery {
|
||||
data_age: number;
|
||||
values: BatteryData[];
|
||||
issues: number[];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
// Huawei
|
||||
// Huawei
|
||||
export interface Huawei {
|
||||
data_age: 0;
|
||||
input_voltage: ValueObject;
|
||||
@ -15,4 +15,4 @@ export interface Huawei {
|
||||
output_power: ValueObject;
|
||||
output_temp: ValueObject;
|
||||
amp_hour: ValueObject;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export interface HuaweiLimitConfig {
|
||||
voltage: number;
|
||||
voltage_valid: boolean;
|
||||
current: number;
|
||||
current_valid: boolean;
|
||||
online: boolean;
|
||||
}
|
||||
voltage: number;
|
||||
voltage_valid: boolean;
|
||||
current: number;
|
||||
current_valid: boolean;
|
||||
online: boolean;
|
||||
}
|
||||
|
||||
@ -55,21 +55,21 @@ export interface Vedirect {
|
||||
}
|
||||
|
||||
export interface Huawei {
|
||||
enabled: boolean;
|
||||
Power: ValueObject;
|
||||
enabled: boolean;
|
||||
Power: ValueObject;
|
||||
}
|
||||
|
||||
export interface Battery {
|
||||
enabled: boolean;
|
||||
soc?: ValueObject;
|
||||
voltage?: ValueObject;
|
||||
power?: ValueObject;
|
||||
current?: ValueObject;
|
||||
enabled: boolean;
|
||||
soc?: ValueObject;
|
||||
voltage?: ValueObject;
|
||||
power?: ValueObject;
|
||||
current?: ValueObject;
|
||||
}
|
||||
|
||||
export interface PowerMeter {
|
||||
enabled: boolean;
|
||||
Power: ValueObject;
|
||||
enabled: boolean;
|
||||
Power: ValueObject;
|
||||
}
|
||||
|
||||
export interface LiveData {
|
||||
|
||||
@ -14,7 +14,7 @@ export interface PowerMeterMqttConfig {
|
||||
export interface PowerMeterSerialSdmConfig {
|
||||
polling_interval: number;
|
||||
address: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PowerMeterHttpJsonValue {
|
||||
http_request: HttpRequestConfig;
|
||||
|
||||
@ -2,4 +2,4 @@ export interface VedirectStatus {
|
||||
vedirect_enabled: boolean;
|
||||
verbose_logging: boolean;
|
||||
vedirect_updatesonly: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,12 @@
|
||||
|
||||
<form @submit="saveChargerConfig">
|
||||
<CardElement :text="$t('acchargeradmin.Configuration')" textVariant="text-bg-primary">
|
||||
<InputElement :label="$t('acchargeradmin.EnableHuawei')"
|
||||
v-model="acChargerConfigList.enabled"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
:label="$t('acchargeradmin.EnableHuawei')"
|
||||
v-model="acChargerConfigList.enabled"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<div class="row mb-3" v-show="acChargerConfigList.enabled">
|
||||
<label class="col-sm-4 col-form-label">
|
||||
@ -16,123 +19,206 @@
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<select class="form-select" v-model="acChargerConfigList.can_controller_frequency">
|
||||
<option v-for="frequency in frequencyTypeList" :key="frequency.key" :value="frequency.value">
|
||||
<option
|
||||
v-for="frequency in frequencyTypeList"
|
||||
:key="frequency.key"
|
||||
:value="frequency.value"
|
||||
>
|
||||
{{ frequency.key }} MHz
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputElement v-show="acChargerConfigList.enabled"
|
||||
:label="$t('acchargeradmin.VerboseLogging')"
|
||||
v-model="acChargerConfigList.verbose_logging"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
v-show="acChargerConfigList.enabled"
|
||||
:label="$t('acchargeradmin.VerboseLogging')"
|
||||
v-model="acChargerConfigList.verbose_logging"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement v-show="acChargerConfigList.enabled"
|
||||
:label="$t('acchargeradmin.EnableAutoPower')"
|
||||
v-model="acChargerConfigList.auto_power_enabled"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
v-show="acChargerConfigList.enabled"
|
||||
:label="$t('acchargeradmin.EnableAutoPower')"
|
||||
v-model="acChargerConfigList.auto_power_enabled"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement v-show="acChargerConfigList.enabled && acChargerConfigList.auto_power_enabled"
|
||||
:label="$t('acchargeradmin.EnableBatterySoCLimits')"
|
||||
v-model="acChargerConfigList.auto_power_batterysoc_limits_enabled"
|
||||
type="checkbox" wide />
|
||||
<InputElement
|
||||
v-show="acChargerConfigList.enabled && acChargerConfigList.auto_power_enabled"
|
||||
:label="$t('acchargeradmin.EnableBatterySoCLimits')"
|
||||
v-model="acChargerConfigList.auto_power_batterysoc_limits_enabled"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement v-show="acChargerConfigList.enabled"
|
||||
:label="$t('acchargeradmin.EnableEmergencyCharge')"
|
||||
v-model="acChargerConfigList.emergency_charge_enabled"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
v-show="acChargerConfigList.enabled"
|
||||
:label="$t('acchargeradmin.EnableEmergencyCharge')"
|
||||
v-model="acChargerConfigList.emergency_charge_enabled"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<CardElement :text="$t('acchargeradmin.Limits')" textVariant="text-bg-primary" add-space
|
||||
v-show="acChargerConfigList.auto_power_enabled || acChargerConfigList.emergency_charge_enabled">
|
||||
<CardElement
|
||||
:text="$t('acchargeradmin.Limits')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
v-show="acChargerConfigList.auto_power_enabled || acChargerConfigList.emergency_charge_enabled"
|
||||
>
|
||||
<div class="row mb-3">
|
||||
<label for="voltageLimit" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.VoltageLimit') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.stopVoltageLimitHint')" />
|
||||
<label for="voltageLimit" class="col-sm-2 col-form-label"
|
||||
>{{ $t('acchargeradmin.VoltageLimit') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.stopVoltageLimitHint')" />
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" step="0.01" class="form-control" id="voltageLimit"
|
||||
placeholder="42" v-model="acChargerConfigList.voltage_limit"
|
||||
aria-describedby="voltageLimitDescription" min="42" max="58.5" required/>
|
||||
<span class="input-group-text" id="voltageLimitDescription">V</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
id="voltageLimit"
|
||||
placeholder="42"
|
||||
v-model="acChargerConfigList.voltage_limit"
|
||||
aria-describedby="voltageLimitDescription"
|
||||
min="42"
|
||||
max="58.5"
|
||||
required
|
||||
/>
|
||||
<span class="input-group-text" id="voltageLimitDescription">V</span>
|
||||
</div>
|
||||
</div>
|
||||
<label for="enableVoltageLimit" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.enableVoltageLimit') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.enableVoltageLimitHint')" />
|
||||
<label for="enableVoltageLimit" class="col-sm-2 col-form-label"
|
||||
>{{ $t('acchargeradmin.enableVoltageLimit') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.enableVoltageLimitHint')" />
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" step="0.01" class="form-control" id="enableVoltageLimit"
|
||||
placeholder="42" v-model="acChargerConfigList.enable_voltage_limit"
|
||||
aria-describedby="enableVoltageLimitDescription" min="42" max="58.5" required/>
|
||||
<span class="input-group-text" id="enableVoltageLimitDescription">V</span>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
id="enableVoltageLimit"
|
||||
placeholder="42"
|
||||
v-model="acChargerConfigList.enable_voltage_limit"
|
||||
aria-describedby="enableVoltageLimitDescription"
|
||||
min="42"
|
||||
max="58.5"
|
||||
required
|
||||
/>
|
||||
<span class="input-group-text" id="enableVoltageLimitDescription">V</span>
|
||||
</div>
|
||||
</div>
|
||||
<label for="lowerPowerLimit" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.lowerPowerLimit') }}:</label>
|
||||
<label for="lowerPowerLimit" class="col-sm-2 col-form-label"
|
||||
>{{ $t('acchargeradmin.lowerPowerLimit') }}:</label
|
||||
>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="lowerPowerLimit"
|
||||
placeholder="150" v-model="acChargerConfigList.lower_power_limit"
|
||||
aria-describedby="lowerPowerLimitDescription" min="50" max="3000" required/>
|
||||
<span class="input-group-text" id="lowerPowerLimitDescription">W</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="lowerPowerLimit"
|
||||
placeholder="150"
|
||||
v-model="acChargerConfigList.lower_power_limit"
|
||||
aria-describedby="lowerPowerLimitDescription"
|
||||
min="50"
|
||||
max="3000"
|
||||
required
|
||||
/>
|
||||
<span class="input-group-text" id="lowerPowerLimitDescription">W</span>
|
||||
</div>
|
||||
</div>
|
||||
<label for="upperPowerLimit" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.upperPowerLimit') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.upperPowerLimitHint')" />
|
||||
<label for="upperPowerLimit" class="col-sm-2 col-form-label"
|
||||
>{{ $t('acchargeradmin.upperPowerLimit') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.upperPowerLimitHint')" />
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="upperPowerLimit"
|
||||
placeholder="2000" v-model="acChargerConfigList.upper_power_limit"
|
||||
aria-describedby="upperPowerLimitDescription" min="100" max="3000" required/>
|
||||
<span class="input-group-text" id="upperPowerLimitDescription">W</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="upperPowerLimit"
|
||||
placeholder="2000"
|
||||
v-model="acChargerConfigList.upper_power_limit"
|
||||
aria-describedby="upperPowerLimitDescription"
|
||||
min="100"
|
||||
max="3000"
|
||||
required
|
||||
/>
|
||||
<span class="input-group-text" id="upperPowerLimitDescription">W</span>
|
||||
</div>
|
||||
</div>
|
||||
<label for="targetPowerConsumption" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.targetPowerConsumption') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.targetPowerConsumptionHint')" />
|
||||
<label for="targetPowerConsumption" class="col-sm-2 col-form-label"
|
||||
>{{ $t('acchargeradmin.targetPowerConsumption') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.targetPowerConsumptionHint')" />
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="targetPowerConsumption"
|
||||
placeholder="0" v-model="acChargerConfigList.target_power_consumption"
|
||||
aria-describedby="targetPowerConsumptionDescription" min="-3000" max="3000" required/>
|
||||
<span class="input-group-text" id="targetPowerConsumptionDescription">W</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="targetPowerConsumption"
|
||||
placeholder="0"
|
||||
v-model="acChargerConfigList.target_power_consumption"
|
||||
aria-describedby="targetPowerConsumptionDescription"
|
||||
min="-3000"
|
||||
max="3000"
|
||||
required
|
||||
/>
|
||||
<span class="input-group-text" id="targetPowerConsumptionDescription">W</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</CardElement>
|
||||
<CardElement :text="$t('acchargeradmin.BatterySoCLimits')" textVariant="text-bg-primary" add-space
|
||||
v-show="acChargerConfigList.auto_power_enabled && acChargerConfigList.auto_power_batterysoc_limits_enabled">
|
||||
<CardElement
|
||||
:text="$t('acchargeradmin.BatterySoCLimits')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
v-show="
|
||||
acChargerConfigList.auto_power_enabled &&
|
||||
acChargerConfigList.auto_power_batterysoc_limits_enabled
|
||||
"
|
||||
>
|
||||
<div class="row mb-3">
|
||||
<label for="stopBatterySoCThreshold" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.StopBatterySoCThreshold') }}:
|
||||
<label for="stopBatterySoCThreshold" class="col-sm-2 col-form-label"
|
||||
>{{ $t('acchargeradmin.StopBatterySoCThreshold') }}:
|
||||
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.StopBatterySoCThresholdHint')" />
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="stopBatterySoCThreshold"
|
||||
placeholder="95" v-model="acChargerConfigList.stop_batterysoc_threshold"
|
||||
aria-describedby="stopBatterySoCThresholdDescription" min="2" max="99" required/>
|
||||
<span class="input-group-text" id="stopBatterySoCThresholdDescription">%</span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
id="stopBatterySoCThreshold"
|
||||
placeholder="95"
|
||||
v-model="acChargerConfigList.stop_batterysoc_threshold"
|
||||
aria-describedby="stopBatterySoCThresholdDescription"
|
||||
min="2"
|
||||
max="99"
|
||||
required
|
||||
/>
|
||||
<span class="input-group-text" id="stopBatterySoCThresholdDescription">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardElement>
|
||||
</CardElement>
|
||||
|
||||
<FormFooter @reload="getChargerConfig"/>
|
||||
<FormFooter @reload="getChargerConfig" />
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
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 { AcChargerConfig } from "@/types/AcChargerConfig";
|
||||
import type { AcChargerConfig } from '@/types/AcChargerConfig';
|
||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
@ -149,11 +235,11 @@ export default defineComponent({
|
||||
return {
|
||||
dataLoading: true,
|
||||
acChargerConfigList: {} as AcChargerConfig,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
alertMessage: '',
|
||||
alertType: 'info',
|
||||
showAlert: false,
|
||||
frequencyTypeList: [
|
||||
{ key: 8, value: 8000000 },
|
||||
{ key: 8, value: 8000000 },
|
||||
{ key: 16, value: 16000000 },
|
||||
],
|
||||
};
|
||||
@ -164,7 +250,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
getChargerConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/huawei/config", { headers: authHeader() })
|
||||
fetch('/api/huawei/config', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.acChargerConfigList = data;
|
||||
@ -175,21 +261,19 @@ export default defineComponent({
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.acChargerConfigList));
|
||||
formData.append('data', JSON.stringify(this.acChargerConfigList));
|
||||
|
||||
fetch("/api/huawei/config", {
|
||||
method: "POST",
|
||||
fetch('/api/huawei/config', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
.then((response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -6,14 +6,18 @@
|
||||
|
||||
<form @submit="saveBatteryConfig">
|
||||
<CardElement :text="$t('batteryadmin.BatteryConfiguration')" textVariant="text-bg-primary">
|
||||
<InputElement :label="$t('batteryadmin.EnableBattery')"
|
||||
v-model="batteryConfigList.enabled"
|
||||
type="checkbox" />
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.EnableBattery')"
|
||||
v-model="batteryConfigList.enabled"
|
||||
type="checkbox"
|
||||
/>
|
||||
|
||||
<InputElement v-show="batteryConfigList.enabled"
|
||||
:label="$t('batteryadmin.VerboseLogging')"
|
||||
v-model="batteryConfigList.verbose_logging"
|
||||
type="checkbox"/>
|
||||
<InputElement
|
||||
v-show="batteryConfigList.enabled"
|
||||
:label="$t('batteryadmin.VerboseLogging')"
|
||||
v-model="batteryConfigList.verbose_logging"
|
||||
type="checkbox"
|
||||
/>
|
||||
|
||||
<div class="row mb-3" v-show="batteryConfigList.enabled">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
@ -29,61 +33,84 @@
|
||||
</div>
|
||||
</CardElement>
|
||||
|
||||
<CardElement v-show="batteryConfigList.enabled && batteryConfigList.provider == 1"
|
||||
:text="$t('batteryadmin.JkBmsConfiguration')" textVariant="text-bg-primary" addSpace>
|
||||
<CardElement
|
||||
v-show="batteryConfigList.enabled && batteryConfigList.provider == 1"
|
||||
:text="$t('batteryadmin.JkBmsConfiguration')"
|
||||
textVariant="text-bg-primary"
|
||||
addSpace
|
||||
>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
{{ $t('batteryadmin.JkBmsInterface') }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="batteryConfigList.jkbms_interface">
|
||||
<option v-for="jkBmsInterface in jkBmsInterfaceTypeList" :key="jkBmsInterface.key" :value="jkBmsInterface.key">
|
||||
<option
|
||||
v-for="jkBmsInterface in jkBmsInterfaceTypeList"
|
||||
:key="jkBmsInterface.key"
|
||||
:value="jkBmsInterface.key"
|
||||
>
|
||||
{{ $t(`batteryadmin.JkBmsInterface` + jkBmsInterface.value) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputElement :label="$t('batteryadmin.PollingInterval')"
|
||||
v-model="batteryConfigList.jkbms_polling_interval"
|
||||
type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/>
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.PollingInterval')"
|
||||
v-model="batteryConfigList.jkbms_polling_interval"
|
||||
type="number"
|
||||
min="2"
|
||||
max="90"
|
||||
step="1"
|
||||
:postfix="$t('batteryadmin.Seconds')"
|
||||
/>
|
||||
</CardElement>
|
||||
|
||||
<template v-if="batteryConfigList.enabled && batteryConfigList.provider == 2">
|
||||
<CardElement :text="$t('batteryadmin.MqttSocConfiguration')" textVariant="text-bg-primary" addSpace>
|
||||
|
||||
<InputElement :label="$t('batteryadmin.MqttSocTopic')"
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.MqttSocTopic')"
|
||||
v-model="batteryConfigList.mqtt_soc_topic"
|
||||
type="text"
|
||||
maxlength="256" />
|
||||
maxlength="256"
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('batteryadmin.MqttJsonPath')"
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.MqttJsonPath')"
|
||||
v-model="batteryConfigList.mqtt_soc_json_path"
|
||||
type="text"
|
||||
maxlength="128"
|
||||
:tooltip="$t('batteryadmin.MqttJsonPathDescription')" />
|
||||
|
||||
:tooltip="$t('batteryadmin.MqttJsonPathDescription')"
|
||||
/>
|
||||
</CardElement>
|
||||
|
||||
<CardElement :text="$t('batteryadmin.MqttVoltageConfiguration')" textVariant="text-bg-primary" addSpace>
|
||||
|
||||
<InputElement :label="$t('batteryadmin.MqttVoltageTopic')"
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.MqttVoltageTopic')"
|
||||
v-model="batteryConfigList.mqtt_voltage_topic"
|
||||
type="text"
|
||||
maxlength="256" />
|
||||
maxlength="256"
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('batteryadmin.MqttJsonPath')"
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.MqttJsonPath')"
|
||||
v-model="batteryConfigList.mqtt_voltage_json_path"
|
||||
type="text"
|
||||
maxlength="128"
|
||||
:tooltip="$t('batteryadmin.MqttJsonPathDescription')" />
|
||||
:tooltip="$t('batteryadmin.MqttJsonPathDescription')"
|
||||
/>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="mqtt_voltage_unit" class="col-sm-2 col-form-label">
|
||||
{{ $t('batteryadmin.MqttVoltageUnit') }}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<select id="mqtt_voltage_unit" class="form-select" v-model="batteryConfigList.mqtt_voltage_unit">
|
||||
<select
|
||||
id="mqtt_voltage_unit"
|
||||
class="form-select"
|
||||
v-model="batteryConfigList.mqtt_voltage_unit"
|
||||
>
|
||||
<option v-for="u in voltageUnitTypeList" :key="u.key" :value="u.key">
|
||||
{{ u.value }}
|
||||
</option>
|
||||
@ -93,18 +120,18 @@
|
||||
</CardElement>
|
||||
</template>
|
||||
|
||||
<FormFooter @reload="getBatteryConfig"/>
|
||||
<FormFooter @reload="getBatteryConfig" />
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import FormFooter from '@/components/FormFooter.vue';
|
||||
import InputElement from '@/components/InputElement.vue';
|
||||
import type { BatteryConfig } from "@/types/BatteryConfig";
|
||||
import type { BatteryConfig } from '@/types/BatteryConfig';
|
||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
@ -120,8 +147,8 @@ export default defineComponent({
|
||||
return {
|
||||
dataLoading: true,
|
||||
batteryConfigList: {} as BatteryConfig,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
alertMessage: '',
|
||||
alertType: 'info',
|
||||
showAlert: false,
|
||||
providerTypeList: [
|
||||
{ key: 0, value: 'PylontechCan' },
|
||||
@ -135,10 +162,10 @@ export default defineComponent({
|
||||
{ key: 1, value: 'Transceiver' },
|
||||
],
|
||||
voltageUnitTypeList: [
|
||||
{ key: 3, value: "mV" },
|
||||
{ key: 2, value: "cV" },
|
||||
{ key: 1, value: "dV" },
|
||||
{ key: 0, value: "V" },
|
||||
{ key: 3, value: 'mV' },
|
||||
{ key: 2, value: 'cV' },
|
||||
{ key: 1, value: 'dV' },
|
||||
{ key: 0, value: 'V' },
|
||||
],
|
||||
};
|
||||
},
|
||||
@ -148,7 +175,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
getBatteryConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/battery/config", { headers: authHeader() })
|
||||
fetch('/api/battery/config', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.batteryConfigList = data;
|
||||
@ -159,21 +186,19 @@ export default defineComponent({
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.batteryConfigList));
|
||||
formData.append('data', JSON.stringify(this.batteryConfigList));
|
||||
|
||||
fetch("/api/battery/config", {
|
||||
method: "POST",
|
||||
fetch('/api/battery/config', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
.then((response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -24,9 +24,11 @@
|
||||
:postfix="$t('dtuadmin.Seconds')"
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('dtuadmin.VerboseLogging')"
|
||||
v-model="dtuConfigList.verbose_logging"
|
||||
type="checkbox"/>
|
||||
<InputElement
|
||||
:label="$t('dtuadmin.VerboseLogging')"
|
||||
v-model="dtuConfigList.verbose_logging"
|
||||
type="checkbox"
|
||||
/>
|
||||
|
||||
<div class="row mb-3" v-if="dtuConfigList.nrf_enabled">
|
||||
<label for="inputNrfPaLevel" class="col-sm-2 col-form-label">
|
||||
|
||||
@ -8,7 +8,13 @@
|
||||
@reload="reloadData"
|
||||
>
|
||||
<HintView :hints="liveData.hints" />
|
||||
<InverterTotalInfo :totalData="liveData.total" :totalVeData="liveData.vedirect" :totalBattData="liveData.battery" :powerMeterData="liveData.power_meter" :huaweiData="liveData.huawei"/>
|
||||
<InverterTotalInfo
|
||||
:totalData="liveData.total"
|
||||
:totalVeData="liveData.vedirect"
|
||||
:totalBattData="liveData.battery"
|
||||
:powerMeterData="liveData.power_meter"
|
||||
:huaweiData="liveData.huawei"
|
||||
/>
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { display: 'none' } : {}]">
|
||||
<div class="nav nav-pills row-cols-sm-1" id="v-pills-tab" role="tablist" aria-orientation="vertical">
|
||||
@ -383,8 +389,8 @@ import InverterChannelInfo from '@/components/InverterChannelInfo.vue';
|
||||
import InverterTotalInfo from '@/components/InverterTotalInfo.vue';
|
||||
import ModalDialog from '@/components/ModalDialog.vue';
|
||||
import VedirectView from '@/components/VedirectView.vue';
|
||||
import HuaweiView from '@/components/HuaweiView.vue'
|
||||
import BatteryView from '@/components/BatteryView.vue'
|
||||
import HuaweiView from '@/components/HuaweiView.vue';
|
||||
import BatteryView from '@/components/BatteryView.vue';
|
||||
import type { DevInfoStatus } from '@/types/DevInfoStatus';
|
||||
import type { EventlogItems } from '@/types/EventlogStatus';
|
||||
import type { GridProfileStatus } from '@/types/GridProfileStatus';
|
||||
@ -433,7 +439,7 @@ export default defineComponent({
|
||||
BIconXCircleFill,
|
||||
VedirectView,
|
||||
HuaweiView,
|
||||
BatteryView
|
||||
BatteryView,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -577,12 +583,22 @@ export default defineComponent({
|
||||
if (event.data != '{}') {
|
||||
const newData = JSON.parse(event.data);
|
||||
|
||||
if (typeof newData.vedirect !== 'undefined') { Object.assign(this.liveData.vedirect, newData.vedirect); }
|
||||
if (typeof newData.huawei !== 'undefined') { Object.assign(this.liveData.huawei, newData.huawei); }
|
||||
if (typeof newData.battery !== 'undefined') { Object.assign(this.liveData.battery, newData.battery); }
|
||||
if (typeof newData.power_meter !== 'undefined') { Object.assign(this.liveData.power_meter, newData.power_meter); }
|
||||
if (typeof newData.vedirect !== 'undefined') {
|
||||
Object.assign(this.liveData.vedirect, newData.vedirect);
|
||||
}
|
||||
if (typeof newData.huawei !== 'undefined') {
|
||||
Object.assign(this.liveData.huawei, newData.huawei);
|
||||
}
|
||||
if (typeof newData.battery !== 'undefined') {
|
||||
Object.assign(this.liveData.battery, newData.battery);
|
||||
}
|
||||
if (typeof newData.power_meter !== 'undefined') {
|
||||
Object.assign(this.liveData.power_meter, newData.power_meter);
|
||||
}
|
||||
|
||||
if (typeof newData.total === 'undefined') { return; }
|
||||
if (typeof newData.total === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(this.liveData.total, newData.total);
|
||||
Object.assign(this.liveData.hints, newData.hints);
|
||||
|
||||
@ -13,10 +13,13 @@
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement v-show="mqttConfigList.mqtt_enabled"
|
||||
:label="$t('mqttadmin.VerboseLogging')"
|
||||
v-model="mqttConfigList.mqtt_verbose_logging"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
v-show="mqttConfigList.mqtt_enabled"
|
||||
:label="$t('mqttadmin.VerboseLogging')"
|
||||
v-model="mqttConfigList.mqtt_verbose_logging"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
v-show="mqttConfigList.mqtt_enabled"
|
||||
|
||||
@ -22,7 +22,11 @@
|
||||
<tr>
|
||||
<th>{{ $t('mqttinfo.VerboseLogging') }}</th>
|
||||
<td>
|
||||
<StatusBadge :status="mqttDataList.mqtt_verbose_logging" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" />
|
||||
<StatusBadge
|
||||
:status="mqttDataList.mqtt_verbose_logging"
|
||||
true_text="mqttinfo.Enabled"
|
||||
false_text="mqttinfo.Disabled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@ -8,13 +8,19 @@
|
||||
{{ $t('powerlimiteradmin.ConfigAlertMessage') }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<CardElement :text="$t('powerlimiteradmin.ConfigHints')" textVariant="text-bg-primary" v-if="getConfigHints().length">
|
||||
<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 === 'requirement'"
|
||||
>{{ $t('powerlimiteradmin.ConfigHintRequirement') }}:</b
|
||||
>
|
||||
<b v-if="hint.severity === 'optional'">{{ $t('powerlimiteradmin.ConfigHintOptional') }}:</b>
|
||||
{{ $t('powerlimiteradmin.ConfigHint' + hint.subject) }}
|
||||
</li>
|
||||
@ -25,91 +31,153 @@
|
||||
|
||||
<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
|
||||
: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.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() && 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/>
|
||||
<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()">
|
||||
<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">
|
||||
<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/>
|
||||
<InputElement
|
||||
:label="$t('powerlimiteradmin.InverterIsSolarPowered')"
|
||||
v-model="powerLimiterConfigList.is_inverter_solar_powered"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement v-show="canUseOverscaling()"
|
||||
:label="$t('powerlimiteradmin.UseOverscalingToCompensateShading')"
|
||||
:tooltip="$t('powerlimiteradmin.UseOverscalingToCompensateShadingHint')"
|
||||
v-model="powerLimiterConfigList.use_overscaling_to_compensate_shading"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
v-show="canUseOverscaling()"
|
||||
:label="$t('powerlimiteradmin.UseOverscalingToCompensateShading')"
|
||||
:tooltip="$t('powerlimiteradmin.UseOverscalingToCompensateShadingHint')"
|
||||
v-model="powerLimiterConfigList.use_overscaling_to_compensate_shading"
|
||||
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">
|
||||
<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.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.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
|
||||
: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/>
|
||||
<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">
|
||||
@ -117,100 +185,200 @@
|
||||
<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">
|
||||
<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
|
||||
{{ 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>
|
||||
<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/>
|
||||
<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.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/>
|
||||
<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
|
||||
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()">
|
||||
<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/>
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<div v-if="!powerLimiterConfigList.ignore_soc">
|
||||
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.BatterySocInfo')"></div>
|
||||
<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.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.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/>
|
||||
<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/>
|
||||
<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/>
|
||||
<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.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/>
|
||||
<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/>
|
||||
<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>
|
||||
<div
|
||||
class="alert alert-secondary"
|
||||
role="alert"
|
||||
v-html="$t('powerlimiteradmin.VoltageLoadCorrectionInfo')"
|
||||
></div>
|
||||
</CardElement>
|
||||
|
||||
<FormFooter @reload="getAllData"/>
|
||||
<FormFooter @reload="getAllData" />
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
@ -218,13 +386,13 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.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";
|
||||
import type { PowerLimiterConfig, PowerLimiterMetaData } from '@/types/PowerLimiterConfig';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -240,8 +408,8 @@ export default defineComponent({
|
||||
dataLoading: true,
|
||||
powerLimiterConfigList: {} as PowerLimiterConfig,
|
||||
powerLimiterMetaData: {} as PowerLimiterMetaData,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
alertMessage: '',
|
||||
alertType: 'info',
|
||||
showAlert: false,
|
||||
configAlert: false,
|
||||
};
|
||||
@ -254,9 +422,13 @@ export default defineComponent({
|
||||
const cfg = this.powerLimiterConfigList;
|
||||
const meta = this.powerLimiterMetaData;
|
||||
|
||||
if (newVal === "") { return; } // do not try to convert the placeholder value
|
||||
if (newVal === '') {
|
||||
return;
|
||||
} // do not try to convert the placeholder value
|
||||
|
||||
if (meta.inverters[newVal] !== undefined) { return; }
|
||||
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
|
||||
@ -274,7 +446,7 @@ export default defineComponent({
|
||||
// previously selected inverter was deleted. marks serial as
|
||||
// invalid, selects placeholder option.
|
||||
cfg.inverter_serial = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getConfigHints() {
|
||||
@ -283,27 +455,29 @@ export default defineComponent({
|
||||
const hints = [];
|
||||
|
||||
if (meta.power_meter_enabled !== true) {
|
||||
hints.push({severity: "optional", subject: "PowerMeterDisabled"});
|
||||
hints.push({ severity: 'optional', subject: 'PowerMeterDisabled' });
|
||||
}
|
||||
|
||||
if (typeof meta.inverters === "undefined" || Object.keys(meta.inverters).length == 0) {
|
||||
hints.push({severity: "requirement", subject: "NoInverter"});
|
||||
if (typeof meta.inverters === 'undefined' || Object.keys(meta.inverters).length == 0) {
|
||||
hints.push({ severity: 'requirement', subject: 'NoInverter' });
|
||||
this.configAlert = true;
|
||||
}
|
||||
else {
|
||||
} 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 (
|
||||
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"});
|
||||
hints.push({ severity: 'optional', subject: 'NoChargeController' });
|
||||
}
|
||||
|
||||
if (!meta.battery_enabled) {
|
||||
hints.push({severity: "optional", subject: "NoBatteryInterface"});
|
||||
hints.push({ severity: 'optional', subject: 'NoBatteryInterface' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,7 +497,9 @@ export default defineComponent({
|
||||
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; }
|
||||
if (!canUse) {
|
||||
cfg.solar_passthrough_enabled = false;
|
||||
}
|
||||
return canUse;
|
||||
},
|
||||
canUseSoCThresholds() {
|
||||
@ -345,17 +521,23 @@ export default defineComponent({
|
||||
const cfg = this.powerLimiterConfigList;
|
||||
const meta = this.powerLimiterMetaData;
|
||||
|
||||
const reset = function() {
|
||||
const reset = function () {
|
||||
cfg.inverter_channel_id = 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (cfg.inverter_serial === '') { return reset(); }
|
||||
if (cfg.inverter_serial === '') {
|
||||
return reset();
|
||||
}
|
||||
|
||||
if (cfg.is_inverter_solar_powered) { return reset(); }
|
||||
if (cfg.is_inverter_solar_powered) {
|
||||
return reset();
|
||||
}
|
||||
|
||||
const inverter = meta.inverters[cfg.inverter_serial];
|
||||
if (inverter === undefined) { return reset(); }
|
||||
if (inverter === undefined) {
|
||||
return reset();
|
||||
}
|
||||
|
||||
if (cfg.inverter_channel_id >= inverter.channels) {
|
||||
reset();
|
||||
@ -365,11 +547,11 @@ export default defineComponent({
|
||||
},
|
||||
getAllData() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/powerlimiter/metadata", { headers: authHeader() })
|
||||
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() })
|
||||
fetch('/api/powerlimiter/config', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.powerLimiterConfigList = data;
|
||||
@ -381,21 +563,19 @@ export default defineComponent({
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.powerLimiterConfigList));
|
||||
formData.append('data', JSON.stringify(this.powerLimiterConfigList));
|
||||
|
||||
fetch("/api/powerlimiter/config", {
|
||||
method: "POST",
|
||||
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;
|
||||
}
|
||||
);
|
||||
.then((response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -5,20 +5,26 @@
|
||||
</BootstrapAlert>
|
||||
|
||||
<form @submit="savePowerMeterConfig">
|
||||
<CardElement :text="$t('powermeteradmin.PowerMeterConfiguration')"
|
||||
textVariant="text-bg-primary">
|
||||
<CardElement :text="$t('powermeteradmin.PowerMeterConfiguration')" textVariant="text-bg-primary">
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.PowerMeterEnable')"
|
||||
v-model="powerMeterConfigList.enabled"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.PowerMeterEnable')"
|
||||
v-model="powerMeterConfigList.enabled"
|
||||
type="checkbox" wide />
|
||||
|
||||
<InputElement v-show="powerMeterConfigList.enabled"
|
||||
:label="$t('powermeteradmin.VerboseLogging')"
|
||||
v-model="powerMeterConfigList.verbose_logging"
|
||||
type="checkbox" wide />
|
||||
<InputElement
|
||||
v-show="powerMeterConfigList.enabled"
|
||||
:label="$t('powermeteradmin.VerboseLogging')"
|
||||
v-model="powerMeterConfigList.verbose_logging"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<div class="row mb-3" v-show="powerMeterConfigList.enabled">
|
||||
<label for="inputPowerMeterSource" class="col-sm-4 col-form-label">{{ $t('powermeteradmin.PowerMeterSource') }}</label>
|
||||
<label for="inputPowerMeterSource" class="col-sm-4 col-form-label">{{
|
||||
$t('powermeteradmin.PowerMeterSource')
|
||||
}}</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="inputPowerMeterSource" class="form-select" v-model="powerMeterConfigList.source">
|
||||
<option v-for="source in powerMeterSourceList" :key="source.key" :value="source.key">
|
||||
@ -35,8 +41,20 @@
|
||||
<h2>{{ $t('powermeteradmin.jsonPathExamplesHeading') }}:</h2>
|
||||
{{ $t('powermeteradmin.jsonPathExamplesExplanation') }}
|
||||
<ul>
|
||||
<li><code>power/total/watts</code> — <code>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 } } }</code></li>
|
||||
<li><code>data/[1]/power</code> — <code>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] } }</code></li>
|
||||
<li>
|
||||
<code>power/total/watts</code> —
|
||||
<code
|
||||
>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 }
|
||||
} }</code
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<code>data/[1]/power</code> —
|
||||
<code
|
||||
>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] }
|
||||
}</code
|
||||
>
|
||||
</li>
|
||||
<li><code>total</code> — <code>{ "othervalue": 66, "total": 123.4 }</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -45,23 +63,28 @@
|
||||
<!-- yarn linter wants us to not combine v-if with v-for, so we need to wrap the CardElements //-->
|
||||
<div v-if="powerMeterConfigList.source === 0">
|
||||
<CardElement
|
||||
v-for="(mqtt, index) in powerMeterConfigList.mqtt.values" v-bind:key="index"
|
||||
:text="$t('powermeteradmin.MqttValue', { valueNumber: index + 1})"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.MqttTopic')"
|
||||
v-for="(mqtt, index) in powerMeterConfigList.mqtt.values"
|
||||
v-bind:key="index"
|
||||
:text="$t('powermeteradmin.MqttValue', { valueNumber: index + 1 })"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
>
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.MqttTopic')"
|
||||
v-model="mqtt.topic"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.mqttJsonPath')"
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.mqttJsonPath')"
|
||||
v-model="mqtt.json_path"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
:tooltip="$t('powermeteradmin.valueJsonPathDescription')"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="mqtt_power_unit" class="col-sm-4 col-form-label">
|
||||
@ -81,27 +104,33 @@
|
||||
v-model="mqtt.sign_inverted"
|
||||
:tooltip="$t('powermeteradmin.valueSignInvertedHint')"
|
||||
type="checkbox"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
</CardElement>
|
||||
</div>
|
||||
|
||||
<CardElement v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2)"
|
||||
:text="$t('powermeteradmin.SDM')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.pollingInterval')"
|
||||
<CardElement
|
||||
v-if="powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2"
|
||||
:text="$t('powermeteradmin.SDM')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
>
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.pollingInterval')"
|
||||
v-model="powerMeterConfigList.serial_sdm.polling_interval"
|
||||
type="number"
|
||||
min=1
|
||||
max=15
|
||||
min="1"
|
||||
max="15"
|
||||
:postfix="$t('powermeteradmin.seconds')"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.sdmaddress')"
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.sdmaddress')"
|
||||
v-model="powerMeterConfigList.serial_sdm.address"
|
||||
type="number"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
</CardElement>
|
||||
|
||||
<div v-if="powerMeterConfigList.source === 3">
|
||||
@ -115,45 +144,54 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<CardElement :text="$t('powermeteradmin.HTTP')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
<InputElement :label="$t('powermeteradmin.httpIndividualRequests')"
|
||||
<CardElement :text="$t('powermeteradmin.HTTP')" textVariant="text-bg-primary" add-space>
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.httpIndividualRequests')"
|
||||
v-model="powerMeterConfigList.http_json.individual_requests"
|
||||
type="checkbox"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.pollingInterval')"
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.pollingInterval')"
|
||||
v-model="powerMeterConfigList.http_json.polling_interval"
|
||||
type="number"
|
||||
min=1
|
||||
max=15
|
||||
min="1"
|
||||
max="15"
|
||||
:postfix="$t('powermeteradmin.seconds')"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
</CardElement>
|
||||
|
||||
<CardElement
|
||||
v-for="(httpJson, index) in powerMeterConfigList.http_json.values"
|
||||
:key="index"
|
||||
:text="$t('powermeteradmin.httpValue', { valueNumber: index + 1 })"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
v-for="(httpJson, index) in powerMeterConfigList.http_json.values"
|
||||
:key="index"
|
||||
:text="$t('powermeteradmin.httpValue', { valueNumber: index + 1 })"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
>
|
||||
<InputElement
|
||||
v-if="index > 0"
|
||||
:label="$t('powermeteradmin.httpEnabled')"
|
||||
v-model="httpJson.enabled"
|
||||
type="checkbox" wide />
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<div v-if="httpJson.enabled || index == 0">
|
||||
<HttpRequestSettings
|
||||
v-model="httpJson.http_request"
|
||||
v-if="index == 0 || powerMeterConfigList.http_json.individual_requests"
|
||||
/>
|
||||
|
||||
<HttpRequestSettings v-model="httpJson.http_request" v-if="index == 0 || powerMeterConfigList.http_json.individual_requests"/>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.valueJsonPath')"
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.valueJsonPath')"
|
||||
v-model="httpJson.json_path"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
:tooltip="$t('powermeteradmin.valueJsonPathDescription')"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="power_unit" class="col-sm-4 col-form-label">
|
||||
@ -173,63 +211,70 @@
|
||||
v-model="httpJson.sign_inverted"
|
||||
:tooltip="$t('powermeteradmin.valueSignInvertedHint')"
|
||||
type="checkbox"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
</div>
|
||||
</CardElement>
|
||||
|
||||
<CardElement
|
||||
:text="$t('powermeteradmin.testHttpJsonHeader')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
|
||||
:text="$t('powermeteradmin.testHttpJsonHeader')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
>
|
||||
<div class="text-center mt-3 mb-3">
|
||||
<button type="button" class="btn btn-primary" @click="testHttpJsonRequest()">
|
||||
{{ $t('powermeteradmin.testHttpJsonRequest') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BootstrapAlert v-model="testHttpJsonRequestAlert.show" dismissible :variant="testHttpJsonRequestAlert.type">
|
||||
<BootstrapAlert
|
||||
v-model="testHttpJsonRequestAlert.show"
|
||||
dismissible
|
||||
:variant="testHttpJsonRequestAlert.type"
|
||||
>
|
||||
{{ testHttpJsonRequestAlert.message }}
|
||||
</BootstrapAlert>
|
||||
</CardElement>
|
||||
</div>
|
||||
|
||||
<div v-if="powerMeterConfigList.source === 6">
|
||||
<CardElement :text="$t('powermeteradmin.HTTP_SML')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.pollingInterval')"
|
||||
<CardElement :text="$t('powermeteradmin.HTTP_SML')" textVariant="text-bg-primary" add-space>
|
||||
<InputElement
|
||||
:label="$t('powermeteradmin.pollingInterval')"
|
||||
v-model="powerMeterConfigList.http_sml.polling_interval"
|
||||
type="number"
|
||||
min=1
|
||||
max=15
|
||||
min="1"
|
||||
max="15"
|
||||
:postfix="$t('powermeteradmin.seconds')"
|
||||
wide />
|
||||
wide
|
||||
/>
|
||||
|
||||
<HttpRequestSettings v-model="powerMeterConfigList.http_sml.http_request" />
|
||||
</CardElement>
|
||||
|
||||
<CardElement
|
||||
:text="$t('powermeteradmin.testHttpSmlHeader')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
:text="$t('powermeteradmin.testHttpSmlHeader')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
>
|
||||
<div class="text-center mt-3 mb-3">
|
||||
<button type="button" class="btn btn-primary" @click="testHttpSmlRequest()">
|
||||
{{ $t('powermeteradmin.testHttpSmlRequest') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3 mb-3">
|
||||
<button type="button" class="btn btn-primary" @click="testHttpSmlRequest()">
|
||||
{{ $t('powermeteradmin.testHttpSmlRequest') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BootstrapAlert v-model="testHttpSmlRequestAlert.show" dismissible :variant="testHttpSmlRequestAlert.type">
|
||||
{{ testHttpSmlRequestAlert.message }}
|
||||
</BootstrapAlert>
|
||||
<BootstrapAlert
|
||||
v-model="testHttpSmlRequestAlert.show"
|
||||
dismissible
|
||||
:variant="testHttpSmlRequestAlert.type"
|
||||
>
|
||||
{{ testHttpSmlRequestAlert.message }}
|
||||
</BootstrapAlert>
|
||||
</CardElement>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormFooter @reload="getPowerMeterConfig"/>
|
||||
|
||||
<FormFooter @reload="getPowerMeterConfig" />
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
@ -237,13 +282,13 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import FormFooter from '@/components/FormFooter.vue';
|
||||
import InputElement from '@/components/InputElement.vue';
|
||||
import HttpRequestSettings from '@/components/HttpRequestSettings.vue';
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { PowerMeterConfig } from "@/types/PowerMeterConfig";
|
||||
import type { PowerMeterConfig } from '@/types/PowerMeterConfig';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -252,7 +297,7 @@ export default defineComponent({
|
||||
CardElement,
|
||||
FormFooter,
|
||||
HttpRequestSettings,
|
||||
InputElement
|
||||
InputElement,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -268,15 +313,23 @@ export default defineComponent({
|
||||
{ key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') },
|
||||
],
|
||||
unitTypeList: [
|
||||
{ key: 1, value: "mW" },
|
||||
{ key: 0, value: "W" },
|
||||
{ key: 2, value: "kW" },
|
||||
{ key: 1, value: 'mW' },
|
||||
{ key: 0, value: 'W' },
|
||||
{ key: 2, value: 'kW' },
|
||||
],
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
alertMessage: '',
|
||||
alertType: 'info',
|
||||
showAlert: false,
|
||||
testHttpJsonRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; },
|
||||
testHttpSmlRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; }
|
||||
testHttpJsonRequestAlert: { message: '', type: '', show: false } as {
|
||||
message: string;
|
||||
type: string;
|
||||
show: boolean;
|
||||
},
|
||||
testHttpSmlRequestAlert: { message: '', type: '', show: false } as {
|
||||
message: string;
|
||||
type: string;
|
||||
show: boolean;
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@ -285,7 +338,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
getPowerMeterConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/powermeter/config", { headers: authHeader() })
|
||||
fetch('/api/powermeter/config', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.powerMeterConfigList = data;
|
||||
@ -296,74 +349,68 @@ export default defineComponent({
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.powerMeterConfigList));
|
||||
formData.append('data', JSON.stringify(this.powerMeterConfigList));
|
||||
|
||||
fetch("/api/powermeter/config", {
|
||||
method: "POST",
|
||||
fetch('/api/powermeter/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;
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
);
|
||||
.then((response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
},
|
||||
testHttpJsonRequest() {
|
||||
this.testHttpJsonRequestAlert = {
|
||||
message: "Triggering HTTP request...",
|
||||
type: "info",
|
||||
message: 'Triggering HTTP request...',
|
||||
type: 'info',
|
||||
show: true,
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.powerMeterConfigList));
|
||||
formData.append('data', JSON.stringify(this.powerMeterConfigList));
|
||||
|
||||
fetch("/api/powermeter/testhttpjsonrequest", {
|
||||
method: "POST",
|
||||
fetch('/api/powermeter/testhttpjsonrequest', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
this.testHttpJsonRequestAlert = {
|
||||
message: response.message,
|
||||
type: response.type,
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
this.testHttpJsonRequestAlert = {
|
||||
message: response.message,
|
||||
type: response.type,
|
||||
show: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
testHttpSmlRequest() {
|
||||
this.testHttpSmlRequestAlert = {
|
||||
message: "Triggering HTTP request...",
|
||||
type: "info",
|
||||
message: 'Triggering HTTP request...',
|
||||
type: 'info',
|
||||
show: true,
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.powerMeterConfigList));
|
||||
formData.append('data', JSON.stringify(this.powerMeterConfigList));
|
||||
|
||||
fetch("/api/powermeter/testhttpsmlrequest", {
|
||||
method: "POST",
|
||||
fetch('/api/powermeter/testhttpsmlrequest', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
this.testHttpSmlRequestAlert = {
|
||||
message: response.message,
|
||||
type: response.type,
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
this.testHttpSmlRequestAlert = {
|
||||
message: response.message,
|
||||
type: response.type,
|
||||
show: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -77,7 +77,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
const fetchUrl =
|
||||
'https://api.github.com/repos/helgeerbe/OpenDTU-OnBattery/compare/' + this.systemDataList.git_hash + '...HEAD';
|
||||
'https://api.github.com/repos/helgeerbe/OpenDTU-OnBattery/compare/' +
|
||||
this.systemDataList.git_hash +
|
||||
'...HEAD';
|
||||
|
||||
fetch(fetchUrl)
|
||||
.then((response) => {
|
||||
|
||||
@ -6,35 +6,47 @@
|
||||
|
||||
<form @submit="saveVedirectConfig">
|
||||
<CardElement :text="$t('vedirectadmin.VedirectConfiguration')" textVariant="text-bg-primary">
|
||||
<InputElement :label="$t('vedirectadmin.EnableVedirect')"
|
||||
v-model="vedirectConfigList.vedirect_enabled"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
:label="$t('vedirectadmin.EnableVedirect')"
|
||||
v-model="vedirectConfigList.vedirect_enabled"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
</CardElement>
|
||||
|
||||
<CardElement :text="$t('vedirectadmin.VedirectParameter')" textVariant="text-bg-primary" add-space
|
||||
v-show="vedirectConfigList.vedirect_enabled">
|
||||
<CardElement
|
||||
:text="$t('vedirectadmin.VedirectParameter')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space
|
||||
v-show="vedirectConfigList.vedirect_enabled"
|
||||
>
|
||||
<InputElement
|
||||
:label="$t('vedirectadmin.VerboseLogging')"
|
||||
v-model="vedirectConfigList.verbose_logging"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
|
||||
<InputElement :label="$t('vedirectadmin.VerboseLogging')"
|
||||
v-model="vedirectConfigList.verbose_logging"
|
||||
type="checkbox" wide/>
|
||||
|
||||
<InputElement :label="$t('vedirectadmin.UpdatesOnly')"
|
||||
v-model="vedirectConfigList.vedirect_updatesonly"
|
||||
type="checkbox" wide/>
|
||||
<InputElement
|
||||
:label="$t('vedirectadmin.UpdatesOnly')"
|
||||
v-model="vedirectConfigList.vedirect_updatesonly"
|
||||
type="checkbox"
|
||||
wide
|
||||
/>
|
||||
</CardElement>
|
||||
|
||||
<FormFooter @reload="getVedirectConfig"/>
|
||||
<FormFooter @reload="getVedirectConfig" />
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import FormFooter from '@/components/FormFooter.vue';
|
||||
import InputElement from '@/components/InputElement.vue';
|
||||
import type { VedirectConfig } from "@/types/VedirectConfig";
|
||||
import type { VedirectConfig } from '@/types/VedirectConfig';
|
||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
@ -50,8 +62,8 @@ export default defineComponent({
|
||||
return {
|
||||
dataLoading: true,
|
||||
vedirectConfigList: {} as VedirectConfig,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
alertMessage: '',
|
||||
alertType: 'info',
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
@ -61,7 +73,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
getVedirectConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/vedirect/config", { headers: authHeader() })
|
||||
fetch('/api/vedirect/config', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.vedirectConfigList = data;
|
||||
@ -72,22 +84,20 @@ export default defineComponent({
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.vedirectConfigList));
|
||||
formData.append('data', JSON.stringify(this.vedirectConfigList));
|
||||
|
||||
fetch("/api/vedirect/config", {
|
||||
method: "POST",
|
||||
fetch('/api/vedirect/config', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
.then((response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -7,19 +7,31 @@
|
||||
<tr>
|
||||
<th>{{ $t('vedirectinfo.Status') }}</th>
|
||||
<td>
|
||||
<StatusBadge :status="vedirectDataList.vedirect_enabled" true_text="vedirectinfo.Enabled" false_text="vedirectinfo.Disabled" />
|
||||
<StatusBadge
|
||||
:status="vedirectDataList.vedirect_enabled"
|
||||
true_text="vedirectinfo.Enabled"
|
||||
false_text="vedirectinfo.Disabled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ $t('vedirectinfo.VerboseLogging') }}</th>
|
||||
<td>
|
||||
<StatusBadge :status="vedirectDataList.verbose_logging" true_text="vedirectinfo.Enabled" false_text="vedirectinfo.Disabled" />
|
||||
<StatusBadge
|
||||
:status="vedirectDataList.verbose_logging"
|
||||
true_text="vedirectinfo.Enabled"
|
||||
false_text="vedirectinfo.Disabled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ $t('vedirectinfo.UpdatesOnly') }}</th>
|
||||
<td>
|
||||
<StatusBadge :status="vedirectDataList.vedirect_updatesonly" true_text="vedirectinfo.UpdatesEnabled" false_text="vedirectinfo.UpdatesDisabled" />
|
||||
<StatusBadge
|
||||
:status="vedirectDataList.vedirect_updatesonly"
|
||||
true_text="vedirectinfo.UpdatesEnabled"
|
||||
false_text="vedirectinfo.UpdatesDisabled"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -33,7 +45,7 @@
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import StatusBadge from '@/components/StatusBadge.vue';
|
||||
import type { VedirectStatus } from "@/types/VedirectStatus";
|
||||
import type { VedirectStatus } from '@/types/VedirectStatus';
|
||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
@ -41,7 +53,7 @@ export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
CardElement,
|
||||
StatusBadge
|
||||
StatusBadge,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -55,7 +67,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
getVedirectInfo() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/vedirect/status", { headers: authHeader() })
|
||||
fetch('/api/vedirect/status', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.vedirectDataList = data;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user