webapp: apply formatter on downstream sources

This commit is contained in:
Bernhard Kirchen 2024-08-01 20:51:16 +02:00
parent 77af085ad3
commit 4334e60030
28 changed files with 1801 additions and 1266 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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',

View File

@ -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;
}

View File

@ -15,4 +15,4 @@ export interface Battery {
data_age: number;
values: BatteryData[];
issues: number[];
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -14,7 +14,7 @@ export interface PowerMeterMqttConfig {
export interface PowerMeterSerialSdmConfig {
polling_interval: number;
address: number;
};
}
export interface PowerMeterHttpJsonValue {
http_request: HttpRequestConfig;

View File

@ -2,4 +2,4 @@ export interface VedirectStatus {
vedirect_enabled: boolean;
verbose_logging: boolean;
vedirect_updatesonly: boolean;
}
}

View File

@ -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;
});
},
},
});

View File

@ -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;
});
},
},
});

View File

@ -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">

View File

@ -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);

View File

@ -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"

View File

@ -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>

View File

@ -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;
});
},
},
});

View File

@ -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> &mdash; <code>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 } } }</code></li>
<li><code>data/[1]/power</code> &mdash; <code>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] } }</code></li>
<li>
<code>power/total/watts</code> &mdash;
<code
>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 }
} }</code
>
</li>
<li>
<code>data/[1]/power</code> &mdash;
<code
>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] }
}</code
>
</li>
<li><code>total</code> &mdash; <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,
};
});
},
},
});

View File

@ -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) => {

View File

@ -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>

View File

@ -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;