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> <template>
<div class="text-center" v-if="dataLoading"> <div class="text-center" v-if="dataLoading">
<div class="spinner-border" role="status"> <div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span> <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> </div>
</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> </template>
<script lang="ts"> <script lang="ts">
@ -128,103 +154,101 @@ import type { ValueObject } from '@/types/LiveDataStatus';
import { handleResponse, authHeader, authUrl } from '@/utils/authentication'; import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
export default defineComponent({ export default defineComponent({
components: { components: {},
}, data() {
data() { return {
return { socket: {} as WebSocket,
socket: {} as WebSocket, heartInterval: 0,
heartInterval: 0, dataAgeInterval: 0,
dataAgeInterval: 0, dataLoading: true,
dataLoading: true, batteryData: {} as Battery,
batteryData: {} as Battery, isFirstFetchAfterConnect: true,
isFirstFetchAfterConnect: true,
alertMessageLimit: "", alertMessageLimit: '',
alertTypeLimit: "info", alertTypeLimit: 'info',
showAlertLimit: false, showAlertLimit: false,
checked: 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;
}, },
getInitialData() { created() {
console.log("Get initalData for Battery"); this.getInitialData();
this.dataLoading = true; this.initSocket();
this.initDataAgeing();
fetch("/api/batterylivedata/status", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.batteryData = data;
this.dataLoading = false;
});
}, },
initSocket() { unmounted() {
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(); this.closeSocket();
};
}, },
initDataAgeing() { methods: {
this.dataAgeInterval = setInterval(() => { isStringValue(value: ValueObject | StringValue): value is StringValue {
if (this.batteryData) { return value && typeof value === 'object' && 'translate' in value;
this.batteryData.data_age++; },
} getInitialData() {
}, 1000); 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 computed: {
heartCheck() { maxIssueValue() {
this.heartInterval && clearTimeout(this.heartInterval); return 'issues' in this.batteryData ? Math.max(...Object.values(this.batteryData.issues)) : 0;
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;
}
},
computed: {
maxIssueValue() {
return ('issues' in this.batteryData)?Math.max(...Object.values(this.batteryData.issues)):0;
},
},
}); });
</script> </script>

View File

@ -1,14 +1,15 @@
<template> <template>
<div> <div>
<InputElement <InputElement
:label="$t('httprequestsettings.url')" :label="$t('httprequestsettings.url')"
v-model="cfg.url" v-model="cfg.url"
type="text" type="text"
maxlength="1024" maxlength="1024"
placeholder="http://admin:supersecret@mypowermeter.home/status" placeholder="http://admin:supersecret@mypowermeter.home/status"
prefix="GET " prefix="GET "
:tooltip="$t('httprequestsettings.urlDescription')" :tooltip="$t('httprequestsettings.urlDescription')"
wide /> wide
/>
<div class="row mb-3"> <div class="row mb-3">
<label for="auth_type" class="col-sm-4 col-form-label">{{ $t('httprequestsettings.authorization') }}</label> <label for="auth_type" class="col-sm-4 col-form-label">{{ $t('httprequestsettings.authorization') }}</label>
@ -22,42 +23,47 @@
</div> </div>
<InputElement <InputElement
v-if="cfg.auth_type != 0" v-if="cfg.auth_type != 0"
:label="$t('httprequestsettings.username')" :label="$t('httprequestsettings.username')"
v-model="cfg.username" v-model="cfg.username"
type="text" type="text"
maxlength="64" maxlength="64"
wide /> wide
/>
<InputElement <InputElement
v-if="cfg.auth_type != 0" v-if="cfg.auth_type != 0"
:label="$t('httprequestsettings.password')" :label="$t('httprequestsettings.password')"
v-model="cfg.password" v-model="cfg.password"
type="password" type="password"
maxlength="64" maxlength="64"
wide /> wide
/>
<InputElement <InputElement
:label="$t('httprequestsettings.headerKey')" :label="$t('httprequestsettings.headerKey')"
v-model="cfg.header_key" v-model="cfg.header_key"
type="text" type="text"
maxlength="64" maxlength="64"
:tooltip="$t('httprequestsettings.headerKeyDescription')" :tooltip="$t('httprequestsettings.headerKeyDescription')"
wide /> wide
/>
<InputElement <InputElement
:label="$t('httprequestsettings.headerValue')" :label="$t('httprequestsettings.headerValue')"
v-model="cfg.header_value" v-model="cfg.header_value"
type="text" type="text"
maxlength="256" maxlength="256"
wide /> wide
/>
<InputElement <InputElement
:label="$t('httprequestsettings.timeout')" :label="$t('httprequestsettings.timeout')"
v-model="cfg.timeout" v-model="cfg.timeout"
type="number" type="number"
:postfix="$t('httprequestsettings.milliSeconds')" :postfix="$t('httprequestsettings.milliSeconds')"
wide /> wide
/>
</div> </div>
</template> </template>
@ -67,28 +73,28 @@ import type { HttpRequestConfig } from '@/types/HttpRequestConfig';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
export default defineComponent({ export default defineComponent({
props: { 'modelValue': Object as () => HttpRequestConfig }, props: { modelValue: Object as () => HttpRequestConfig },
computed: { computed: {
cfg: { cfg: {
get(): HttpRequestConfig { get(): HttpRequestConfig {
return this.modelValue || {} as HttpRequestConfig; return this.modelValue || ({} as HttpRequestConfig);
}, },
set(newValue: HttpRequestConfig): void { set(newValue: HttpRequestConfig): void {
this.$emit('update:modelValue', newValue); this.$emit('update:modelValue', newValue);
} },
} },
}, },
components: { components: {
InputElement InputElement,
}, },
data() { data() {
return { return {
authTypeList: [ authTypeList: [
{ key: 0, value: "None" }, { key: 0, value: 'None' },
{ key: 1, value: "Basic" }, { key: 1, value: 'Basic' },
{ key: 2, value: "Digest" }, { key: 2, value: 'Digest' },
] ],
}; };
} },
}); });
</script> </script>

View File

@ -1,205 +1,270 @@
<template> <template>
<div class="text-center" v-if="dataLoading"> <div class="text-center" v-if="dataLoading">
<div class="spinner-border" role="status"> <div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span> <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> </div>
</div>
</div> </div>
<div class="modal" id="huaweiLimitSettingView" ref="huaweiLimitSettingView" tabindex="-1"> <template v-else>
<div class="modal-dialog modal-lg"> <div class="row gy-3 mt-0">
<div class="modal-content"> <div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
<form @submit="onSubmitLimit"> <div class="card">
<div class="modal-header"> <div
<h5 class="modal-title">{{ $t('huawei.LimitSettings') }}</h5> class="card-header d-flex justify-content-between align-items-center"
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> :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>
<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>
</div> <div class="modal" id="huaweiLimitSettingView" ref="huaweiLimitSettingView" tabindex="-1">
</template> <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> </template>
<script lang="ts"> <script lang="ts">
@ -209,150 +274,143 @@ import type { HuaweiLimitConfig } from '@/types/HuaweiLimitConfig';
import { handleResponse, authHeader, authUrl } from '@/utils/authentication'; import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
import { import { BIconSpeedometer } from 'bootstrap-icons-vue';
BIconSpeedometer,
} from 'bootstrap-icons-vue';
export default defineComponent({ export default defineComponent({
components: { components: {
BIconSpeedometer 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;
});
}, },
initSocket() { data() {
console.log("Starting connection to Huawei WebSocket Server"); 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; alertMessageLimit: '',
const authString = authUrl(); alertTypeLimit: 'info',
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws" showAlertLimit: false,
}://${authString}${host}/huaweilivedata`; checked: false,
};
this.socket = new WebSocket(webSocketUrl); },
created() {
this.socket.onmessage = (event) => { this.getInitialData();
console.log(event); this.initSocket();
this.huaweiData = JSON.parse(event.data); this.initDataAgeing();
this.dataLoading = false; },
this.heartCheck(); // Reset heartbeat detection unmounted() {
};
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(); this.closeSocket();
};
}, },
initDataAgeing() { methods: {
this.dataAgeInterval = setInterval(() => { getInitialData() {
if (this.huaweiData) { console.log('Get initalData for Huawei');
this.huaweiData.data_age++; this.dataLoading = true;
}
}, 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(); fetch('/api/huaweilivedata/status', { headers: authHeader() })
formData.append("data", JSON.stringify(this.targetLimitList)); .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", { this.socket = new WebSocket(webSocketUrl);
method: "POST",
headers: authHeader(), this.socket.onmessage = (event) => {
body: formData, console.log(event);
}) this.huaweiData = JSON.parse(event.data);
.then((response) => handleResponse(response, this.$emitter, this.$router)) this.dataLoading = false;
.then( this.heartCheck(); // Reset heartbeat detection
(response) => { };
if (response.type == "success") {
this.huaweiLimitSettingView.hide(); this.socket.onopen = function (event) {
} else { console.log(event);
this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param); console.log('Successfully connected to the Huawei websocket server...');
this.alertTypeLimit = response.type; };
this.showAlertLimit = true;
} // 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> <template>
<div class="row row-cols-1 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-md-3 g-3">
<div class="col" v-if="totalVeData.enabled"> <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> <h2>
{{ $n(totalVeData.total.YieldTotal.v, 'decimal', { {{
minimumFractionDigits: totalVeData.total.YieldTotal.d, $n(totalVeData.total.YieldTotal.v, 'decimal', {
maximumFractionDigits:totalVeData.total.YieldTotal.d minimumFractionDigits: totalVeData.total.YieldTotal.d,
}) }} maximumFractionDigits: totalVeData.total.YieldTotal.d,
})
}}
<small class="text-muted">{{ totalVeData.total.YieldTotal.u }}</small> <small class="text-muted">{{ totalVeData.total.YieldTotal.u }}</small>
</h2> </h2>
</CardElement> </CardElement>
@ -14,10 +20,12 @@
<div class="col" v-if="totalVeData.enabled"> <div class="col" v-if="totalVeData.enabled">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.MpptTotalYieldDay')"> <CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.MpptTotalYieldDay')">
<h2> <h2>
{{ $n(totalVeData.total.YieldDay.v, 'decimal', { {{
minimumFractionDigits: totalVeData.total.YieldDay.d, $n(totalVeData.total.YieldDay.v, 'decimal', {
maximumFractionDigits: totalVeData.total.YieldDay.d minimumFractionDigits: totalVeData.total.YieldDay.d,
}) }} maximumFractionDigits: totalVeData.total.YieldDay.d,
})
}}
<small class="text-muted">{{ totalVeData.total.YieldDay.u }}</small> <small class="text-muted">{{ totalVeData.total.YieldDay.u }}</small>
</h2> </h2>
</CardElement> </CardElement>
@ -25,16 +33,22 @@
<div class="col" v-if="totalVeData.enabled"> <div class="col" v-if="totalVeData.enabled">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.MpptTotalPower')"> <CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.MpptTotalPower')">
<h2> <h2>
{{ $n(totalVeData.total.Power.v, 'decimal', { {{
minimumFractionDigits: totalVeData.total.Power.d, $n(totalVeData.total.Power.v, 'decimal', {
maximumFractionDigits: totalVeData.total.Power.d minimumFractionDigits: totalVeData.total.Power.d,
}) }} maximumFractionDigits: totalVeData.total.Power.d,
})
}}
<small class="text-muted">{{ totalVeData.total.Power.u }}</small> <small class="text-muted">{{ totalVeData.total.Power.u }}</small>
</h2> </h2>
</CardElement> </CardElement>
</div> </div>
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.InverterTotalYieldTotal')"> <CardElement
centerContent
textVariant="text-bg-success"
:text="$t('invertertotalinfo.InverterTotalYieldTotal')"
>
<h2> <h2>
{{ {{
$n(totalData.YieldTotal.v, 'decimal', { $n(totalData.YieldTotal.v, 'decimal', {
@ -47,7 +61,11 @@
</CardElement> </CardElement>
</div> </div>
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.InverterTotalYieldDay')"> <CardElement
centerContent
textVariant="text-bg-success"
:text="$t('invertertotalinfo.InverterTotalYieldDay')"
>
<h2> <h2>
{{ {{
$n(totalData.YieldDay.v, 'decimal', { $n(totalData.YieldDay.v, 'decimal', {
@ -74,46 +92,64 @@
</div> </div>
<template v-if="totalBattData.enabled"> <template v-if="totalBattData.enabled">
<div class="col"> <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"> <div class="flex-fill" v-if="totalBattData.soc">
<h2> <h2>
{{ $n(totalBattData.soc.v, 'decimal', { {{
minimumFractionDigits: totalBattData.soc.d, $n(totalBattData.soc.v, 'decimal', {
maximumFractionDigits: totalBattData.soc.d minimumFractionDigits: totalBattData.soc.d,
}) }} maximumFractionDigits: totalBattData.soc.d,
})
}}
<small class="text-muted">{{ totalBattData.soc.u }}</small> <small class="text-muted">{{ totalBattData.soc.u }}</small>
</h2> </h2>
</div> </div>
<div class="flex-fill" v-if="totalBattData.voltage"> <div class="flex-fill" v-if="totalBattData.voltage">
<h2> <h2>
{{ $n(totalBattData.voltage.v, 'decimal', { {{
minimumFractionDigits: totalBattData.voltage.d, $n(totalBattData.voltage.v, 'decimal', {
maximumFractionDigits: totalBattData.voltage.d minimumFractionDigits: totalBattData.voltage.d,
}) }} maximumFractionDigits: totalBattData.voltage.d,
})
}}
<small class="text-muted">{{ totalBattData.voltage.u }}</small> <small class="text-muted">{{ totalBattData.voltage.u }}</small>
</h2> </h2>
</div> </div>
</CardElement> </CardElement>
</div> </div>
<div class="col" v-if="totalBattData.power || totalBattData.current"> <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"> <div class="flex-fill" v-if="totalBattData.power">
<h2> <h2>
{{ $n(totalBattData.power.v, 'decimal', { {{
minimumFractionDigits: totalBattData.power.d, $n(totalBattData.power.v, 'decimal', {
maximumFractionDigits: totalBattData.power.d minimumFractionDigits: totalBattData.power.d,
}) }} maximumFractionDigits: totalBattData.power.d,
})
}}
<small class="text-muted">{{ totalBattData.power.u }}</small> <small class="text-muted">{{ totalBattData.power.u }}</small>
</h2> </h2>
</div> </div>
<div class="flex-fill" v-if="totalBattData.current"> <div class="flex-fill" v-if="totalBattData.current">
<h2> <h2>
{{ $n(totalBattData.current.v, 'decimal', { {{
minimumFractionDigits: totalBattData.current.d, $n(totalBattData.current.v, 'decimal', {
maximumFractionDigits: totalBattData.current.d minimumFractionDigits: totalBattData.current.d,
}) }} maximumFractionDigits: totalBattData.current.d,
})
}}
<small class="text-muted">{{ totalBattData.current.u }}</small> <small class="text-muted">{{ totalBattData.current.u }}</small>
</h2> </h2>
</div> </div>
@ -123,22 +159,26 @@
<div class="col" v-if="powerMeterData.enabled"> <div class="col" v-if="powerMeterData.enabled">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HomePower')"> <CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HomePower')">
<h2> <h2>
{{ $n(powerMeterData.Power.v, 'decimal', { {{
minimumFractionDigits: powerMeterData.Power.d, $n(powerMeterData.Power.v, 'decimal', {
maximumFractionDigits: powerMeterData.Power.d minimumFractionDigits: powerMeterData.Power.d,
}) }} maximumFractionDigits: powerMeterData.Power.d,
<small class="text-muted">{{powerMeterData.Power.u }}</small> })
}}
<small class="text-muted">{{ powerMeterData.Power.u }}</small>
</h2> </h2>
</CardElement> </CardElement>
</div> </div>
<div class="col" v-if="huaweiData.enabled"> <div class="col" v-if="huaweiData.enabled">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HuaweiPower')"> <CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HuaweiPower')">
<h2> <h2>
{{ $n(huaweiData.Power.v, 'decimal', { {{
minimumFractionDigits: huaweiData.Power.d, $n(huaweiData.Power.v, 'decimal', {
maximumFractionDigits: huaweiData.Power.d minimumFractionDigits: huaweiData.Power.d,
}) }} maximumFractionDigits: huaweiData.Power.d,
<small class="text-muted">{{huaweiData.Power.u }}</small> })
}}
<small class="text-muted">{{ huaweiData.Power.u }}</small>
</h2> </h2>
</CardElement> </CardElement>
</div> </div>
@ -154,12 +194,12 @@ export default defineComponent({
components: { components: {
CardElement, CardElement,
}, },
props: { props: {
totalData: { type: Object as PropType<Total>, required: true }, totalData: { type: Object as PropType<Total>, required: true },
totalVeData: { type: Object as PropType<Vedirect>, required: true }, totalVeData: { type: Object as PropType<Vedirect>, required: true },
totalBattData: { type: Object as PropType<Battery>, required: true }, totalBattData: { type: Object as PropType<Battery>, required: true },
powerMeterData: { type: Object as PropType<PowerMeter>, required: true }, powerMeterData: { type: Object as PropType<PowerMeter>, required: true },
huaweiData: { type: Object as PropType<Huawei>, required: true }, huaweiData: { type: Object as PropType<Huawei>, required: true },
}, },
}); });
</script> </script>

View File

@ -9,7 +9,9 @@
<BIconSun v-else width="30" height="30" class="d-inline-block align-text-top text-warning" /> <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 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> </router-link>
<button <button
class="navbar-toggler" class="navbar-toggler"
@ -69,20 +71,30 @@
$t('menu.DTUSettings') $t('menu.DTUSettings')
}}</router-link> }}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{ $t('menu.VedirectSettings') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{
$t('menu.VedirectSettings')
}}</router-link>
</li> </li>
<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>
<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>
<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>
<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>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/device">{{ <router-link @click="onClick" class="dropdown-item" to="/settings/device">{{
@ -142,7 +154,9 @@
}}</router-link> }}</router-link>
</li> </li>
<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>
<li> <li>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />

View File

@ -1,5 +1,4 @@
<template> <template>
<div class="text-center" v-if="dataLoading"> <div class="text-center" v-if="dataLoading">
<div class="spinner-border" role="status"> <div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span> <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="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="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
<div class="card"> <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="{ :class="{
'text-bg-danger': item.data_age_ms >= 10000, 'text-bg-danger': item.data_age_ms >= 10000,
'text-bg-primary': item.data_age_ms < 10000, 'text-bg-primary': item.data_age_ms < 10000,
}"> }"
>
<div class="p-1 flex-grow-1"> <div class="p-1 flex-grow-1">
<div class="d-flex flex-wrap"> <div class="d-flex flex-wrap">
<div style="padding-right: 2em;"> <div style="padding-right: 2em">
{{ item.product_id }} {{ item.product_id }}
</div> </div>
<div style="padding-right: 2em;"> <div style="padding-right: 2em">
{{ $t('vedirecthome.SerialNumber') }}: {{ serial }} {{ $t('vedirecthome.SerialNumber') }}: {{ serial }}
</div> </div>
<div style="padding-right: 2em;"> <div style="padding-right: 2em">
{{ $t('vedirecthome.FirmwareVersion') }}: {{ item.firmware_version }} {{ $t('vedirecthome.FirmwareVersion') }}: {{ item.firmware_version }}
</div> </div>
<div style="padding-right: 2em;"> <div style="padding-right: 2em">
{{ $t('vedirecthome.DataAge') }}: {{ $t('vedirecthome.Seconds', {'val': Math.floor(item.data_age_ms / 1000)}) }} {{ $t('vedirecthome.DataAge') }}:
{{ $t('vedirecthome.Seconds', { val: Math.floor(item.data_age_ms / 1000) }) }}
</div> </div>
</div> </div>
</div> </div>
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button type="button" <button
class="btn btn-sm" v-tooltip :title="$t('vedirecthome.PowerLimiterState')"> type="button"
class="btn btn-sm"
v-tooltip
:title="$t('vedirecthome.PowerLimiterState')"
>
<div v-if="dplData.PLSTATE == 0"> <div v-if="dplData.PLSTATE == 0">
<BIconXCircleFill style="font-size:24px;" /> <BIconXCircleFill style="font-size: 24px" />
</div> </div>
<div v-else-if="dplData.PLSTATE == 1"> <div v-else-if="dplData.PLSTATE == 1">
<BIconBatteryCharging style="font-size:24px;" /> <BIconBatteryCharging style="font-size: 24px" />
</div> </div>
<div v-else-if="dplData.PLSTATE == 2"> <div v-else-if="dplData.PLSTATE == 2">
<BIconSun style="font-size:24px;" /> <BIconSun style="font-size: 24px" />
</div> </div>
<div v-else-if="dplData.PLSTATE == 3"> <div v-else-if="dplData.PLSTATE == 3">
<BIconBatteryHalf style="font-size:24px;" /> <BIconBatteryHalf style="font-size: 24px" />
</div> </div>
<span v-if="dplData.PLSTATE != -1" <span
class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-info"> v-if="dplData.PLSTATE != -1"
class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-info"
>
{{ dplData.PLLIMIT }} W {{ dplData.PLLIMIT }} W
</span> </span>
</button> </button>
@ -56,34 +64,42 @@
<div class="card-body"> <div class="card-body">
<div class="row flex-row flex-wrap align-items-start g-3"> <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 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="card" :class="{ 'border-info': section === 'device' }">
<div :class="(section === 'device')?'card-header text-bg-info':'card-header'">{{ $t('vedirecthome.section_' + section) }}</div> <div :class="section === 'device' ? 'card-header text-bg-info' : 'card-header'">
{{ $t('vedirecthome.section_' + section) }}
</div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col">{{ $t('vedirecthome.Property') }}</th> <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> <th scope="col">{{ $t('vedirecthome.Unit') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(prop, key) in values" v-bind:key="key"> <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"> <td style="text-align: right">
<template v-if="typeof prop === 'string'"> <template v-if="typeof prop === 'string'">
{{ prop }} {{ prop }}
</template> </template>
<template v-else> <template v-else>
{{ $n(prop.v, 'decimal', { {{
minimumFractionDigits: prop.d, $n(prop.v, 'decimal', {
maximumFractionDigits: prop.d}) minimumFractionDigits: prop.d,
maximumFractionDigits: prop.d,
})
}} }}
</template> </template>
</td> </td>
<td v-if="typeof prop === 'string'"></td> <td v-if="typeof prop === 'string'"></td>
<td v-else>{{prop.u}}</td> <td v-else>{{ prop.u }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -97,29 +113,20 @@
</div> </div>
</div> </div>
</template> </template>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { DynamicPowerLimiter, Vedirect } from '@/types/VedirectLiveDataStatus'; import type { DynamicPowerLimiter, Vedirect } from '@/types/VedirectLiveDataStatus';
import { handleResponse, authHeader, authUrl } from '@/utils/authentication'; import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
import { import { BIconSun, BIconBatteryCharging, BIconBatteryHalf, BIconXCircleFill } from 'bootstrap-icons-vue';
BIconSun,
BIconBatteryCharging,
BIconBatteryHalf,
BIconXCircleFill
} from 'bootstrap-icons-vue';
export default defineComponent({ export default defineComponent({
components: { components: {
BIconSun, BIconSun,
BIconBatteryCharging, BIconBatteryCharging,
BIconBatteryHalf, BIconBatteryHalf,
BIconXCircleFill BIconXCircleFill,
}, },
data() { data() {
return { return {
@ -141,44 +148,43 @@ export default defineComponent({
}, },
methods: { methods: {
getInitialData() { getInitialData() {
console.log("Get initalData for VeDirect"); console.log('Get initalData for VeDirect');
this.dataLoading = true; this.dataLoading = true;
fetch("/api/vedirectlivedata/status", { headers: authHeader() }) fetch('/api/vedirectlivedata/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((root) => { .then((root) => {
this.dplData = root["dpl"]; this.dplData = root['dpl'];
this.vedirect = root["vedirect"]; this.vedirect = root['vedirect'];
this.dataLoading = false; this.dataLoading = false;
this.resetDataAging(Object.keys(root["vedirect"]["instances"])); this.resetDataAging(Object.keys(root['vedirect']['instances']));
}); });
}, },
initSocket() { initSocket() {
console.log("Starting connection to VeDirect WebSocket Server"); console.log('Starting connection to VeDirect WebSocket Server');
const { protocol, host } = location; const { protocol, host } = location;
const authString = authUrl(); const authString = authUrl();
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws" const webSocketUrl = `${protocol === 'https:' ? 'wss' : 'ws'}://${authString}${host}/vedirectlivedata`;
}://${authString}${host}/vedirectlivedata`;
this.socket = new WebSocket(webSocketUrl); this.socket = new WebSocket(webSocketUrl);
this.socket.onmessage = (event) => { this.socket.onmessage = (event) => {
console.log(event); console.log(event);
const root = JSON.parse(event.data); const root = JSON.parse(event.data);
this.dplData = root["dpl"]; this.dplData = root['dpl'];
if (root["vedirect"]["full_update"] === true) { if (root['vedirect']['full_update'] === true) {
this.vedirect = root["vedirect"]; this.vedirect = root['vedirect'];
} else { } 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.dataLoading = false;
this.heartCheck(); // Reset heartbeat detection this.heartCheck(); // Reset heartbeat detection
}; };
this.socket.onopen = function (event) { this.socket.onopen = function (event) {
console.log(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 // 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) { 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; this.vedirect.instances[serial].data_age_ms += 1000;
@ -213,7 +221,7 @@ export default defineComponent({
this.heartInterval = setInterval(() => { this.heartInterval = setInterval(() => {
if (this.socket.readyState === 1) { if (this.socket.readyState === 1) {
// Connection status // Connection status
this.socket.send("ping"); this.socket.send('ping');
} else { } else {
this.initSocket(); // Breakpoint reconnection 5 Time this.initSocket(); // Breakpoint reconnection 5 Time
} }

View File

@ -366,7 +366,7 @@
"Disconnected": "getrennt" "Disconnected": "getrennt"
}, },
"vedirectinfo": { "vedirectinfo": {
"VedirectInformation" : "VE.Direct Info", "VedirectInformation": "VE.Direct Info",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status", "Status": "@:ntpinfo.Status",
"Enabled": "@:mqttinfo.Enabled", "Enabled": "@:mqttinfo.Enabled",
@ -553,7 +553,7 @@
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden" "UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden"
}, },
"powermeteradmin":{ "powermeteradmin": {
"PowerMeterSettings": "Stromzähler Einstellungen", "PowerMeterSettings": "Stromzähler Einstellungen",
"PowerMeterConfiguration": "Stromzähler Konfiguration", "PowerMeterConfiguration": "Stromzähler Konfiguration",
"PowerMeterEnable": "Aktiviere Stromzähler", "PowerMeterEnable": "Aktiviere Stromzähler",

View File

@ -368,7 +368,7 @@
"Disconnected": "disconnected" "Disconnected": "disconnected"
}, },
"vedirectinfo": { "vedirectinfo": {
"VedirectInformation" : "VE.Direct Info", "VedirectInformation": "VE.Direct Info",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status", "Status": "@:ntpinfo.Status",
"Enabled": "@:mqttinfo.Enabled", "Enabled": "@:mqttinfo.Enabled",
@ -555,7 +555,7 @@
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"UpdatesOnly": "Publish values to MQTT only when they change" "UpdatesOnly": "Publish values to MQTT only when they change"
}, },
"powermeteradmin":{ "powermeteradmin": {
"PowerMeterSettings": "Power Meter Settings", "PowerMeterSettings": "Power Meter Settings",
"PowerMeterConfiguration": "Power Meter Configuration", "PowerMeterConfiguration": "Power Meter Configuration",
"PowerMeterEnable": "Enable Power Meter", "PowerMeterEnable": "Enable Power Meter",
@ -847,7 +847,7 @@
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum", "format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format" "format_unknown": "Unknown format"
}, },
"huawei": { "huawei": {
"DataAge": "Data Age: ", "DataAge": "Data Age: ",
"Seconds": " {val} seconds", "Seconds": " {val} seconds",

View File

@ -401,7 +401,7 @@
"Disconnected": "déconnecté" "Disconnected": "déconnecté"
}, },
"vedirectinfo": { "vedirectinfo": {
"VedirectInformation" : "VE.Direct Info", "VedirectInformation": "VE.Direct Info",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary", "ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status", "Status": "@:ntpinfo.Status",
"Enabled": "@:mqttinfo.Enabled", "Enabled": "@:mqttinfo.Enabled",
@ -681,54 +681,54 @@
"Cancel": "@:base.Cancel" "Cancel": "@:base.Cancel"
}, },
"powerlimiteradmin": { "powerlimiteradmin": {
"PowerLimiterSettings": "Dynamic Power Limiter Settings", "PowerLimiterSettings": "Dynamic Power Limiter Settings",
"ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.", "ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.",
"ConfigHints": "Configuration Notes", "ConfigHints": "Configuration Notes",
"ConfigHintRequirement": "Required", "ConfigHintRequirement": "Required",
"ConfigHintOptional": "Optional", "ConfigHintOptional": "Optional",
"ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", "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).", "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.", "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.", "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.", "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.", "ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.",
"General": "General", "General": "General",
"Enable": "Enable", "Enable": "Enable",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"SolarPassthrough": "Solar-Passthrough", "SolarPassthrough": "Solar-Passthrough",
"EnableSolarPassthrough": "Enable Solar-Passthrough", "EnableSolarPassthrough": "Enable Solar-Passthrough",
"SolarPassthroughLosses": "(Full) Solar-Passthrough Losses", "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.", "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", "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.", "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", "InverterSettings": "Inverter",
"Inverter": "Target Inverter", "Inverter": "Target Inverter",
"SelectInverter": "Select an inverter...", "SelectInverter": "Select an inverter...",
"InverterChannelId": "Input used for voltage measurements", "InverterChannelId": "Input used for voltage measurements",
"TargetPowerConsumption": "Target Grid Consumption", "TargetPowerConsumption": "Target Grid Consumption",
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
"TargetPowerConsumptionHysteresis": "Hysteresis", "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.", "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", "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.", "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", "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.", "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", "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.", "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", "SocThresholds": "Battery State of Charge (SoC) Thresholds",
"IgnoreSoc": "Ignore Battery SoC", "IgnoreSoc": "Ignore Battery SoC",
"StartThreshold": "Start Threshold for Battery Discharging", "StartThreshold": "Start Threshold for Battery Discharging",
"StopThreshold": "Stop Threshold for Battery Discharging", "StopThreshold": "Stop Threshold for Battery Discharging",
"FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold", "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.", "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", "VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold",
"VoltageLoadCorrectionFactor": "Load correction factor", "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.", "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", "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.", "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", "InverterIsSolarPowered": "Inverter is powered by solar modules",
"VoltageThresholds": "Battery Voltage Thresholds", "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)." "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": {
"Login": "Connexion", "Login": "Connexion",

View File

@ -8,10 +8,10 @@ import DtuAdminView from '@/views/DtuAdminView.vue';
import ErrorView from '@/views/ErrorView.vue'; import ErrorView from '@/views/ErrorView.vue';
import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue'; import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue';
import HomeView from '@/views/HomeView.vue'; import HomeView from '@/views/HomeView.vue';
import VedirectAdminView from '@/views/VedirectAdminView.vue' import VedirectAdminView from '@/views/VedirectAdminView.vue';
import PowerMeterAdminView from '@/views/PowerMeterAdminView.vue' import PowerMeterAdminView from '@/views/PowerMeterAdminView.vue';
import PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue' import PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue';
import VedirectInfoView from '@/views/VedirectInfoView.vue' import VedirectInfoView from '@/views/VedirectInfoView.vue';
import InverterAdminView from '@/views/InverterAdminView.vue'; import InverterAdminView from '@/views/InverterAdminView.vue';
import LoginView from '@/views/LoginView.vue'; import LoginView from '@/views/LoginView.vue';
import MaintenanceRebootView from '@/views/MaintenanceRebootView.vue'; import MaintenanceRebootView from '@/views/MaintenanceRebootView.vue';
@ -74,11 +74,11 @@ const router = createRouter({
name: 'Web Console', name: 'Web Console',
component: ConsoleInfoView, component: ConsoleInfoView,
}, },
{ {
path: '/info/vedirect', path: '/info/vedirect',
name: 'VE.Direct', name: 'VE.Direct',
component: VedirectInfoView component: VedirectInfoView,
}, },
{ {
path: '/settings/network', path: '/settings/network',
name: 'Network Settings', name: 'Network Settings',
@ -89,31 +89,31 @@ const router = createRouter({
name: 'NTP Settings', name: 'NTP Settings',
component: NtpAdminView, component: NtpAdminView,
}, },
{ {
path: '/settings/vedirect', path: '/settings/vedirect',
name: 'VE.Direct Settings', name: 'VE.Direct Settings',
component: VedirectAdminView component: VedirectAdminView,
}, },
{ {
path: '/settings/powermeter', path: '/settings/powermeter',
name: 'Power meter Settings', name: 'Power meter Settings',
component: PowerMeterAdminView component: PowerMeterAdminView,
}, },
{ {
path: '/settings/powerlimiter', path: '/settings/powerlimiter',
name: 'Power limiter Settings', name: 'Power limiter Settings',
component: PowerLimiterAdminView component: PowerLimiterAdminView,
}, },
{ {
path: '/settings/battery', path: '/settings/battery',
name: 'Battery Settings', name: 'Battery Settings',
component: BatteryAdminView component: BatteryAdminView,
}, },
{ {
path: '/settings/chargerac', path: '/settings/chargerac',
name: 'Charger Settings', name: 'Charger Settings',
component: AcChargerAdminView component: AcChargerAdminView,
}, },
{ {
path: '/settings/mqtt', path: '/settings/mqtt',
name: 'MqTT Settings', name: 'MqTT Settings',

View File

@ -1,14 +1,14 @@
export interface AcChargerConfig { export interface AcChargerConfig {
enabled: boolean; enabled: boolean;
verbose_logging: boolean; verbose_logging: boolean;
can_controller_frequency: number; can_controller_frequency: number;
auto_power_enabled: boolean; auto_power_enabled: boolean;
auto_power_batterysoc_limits_enabled: boolean; auto_power_batterysoc_limits_enabled: boolean;
voltage_limit: number; voltage_limit: number;
enable_voltage_limit: number; enable_voltage_limit: number;
lower_power_limit: number; lower_power_limit: number;
upper_power_limit: number; upper_power_limit: number;
emergency_charge_enabled: boolean; emergency_charge_enabled: boolean;
stop_batterysoc_threshold: number; stop_batterysoc_threshold: number;
target_power_consumption: number; target_power_consumption: number;
} }

View File

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

View File

@ -1,6 +1,6 @@
import type { ValueObject } from '@/types/LiveDataStatus'; import type { ValueObject } from '@/types/LiveDataStatus';
// Huawei // Huawei
export interface Huawei { export interface Huawei {
data_age: 0; data_age: 0;
input_voltage: ValueObject; input_voltage: ValueObject;
@ -15,4 +15,4 @@ export interface Huawei {
output_power: ValueObject; output_power: ValueObject;
output_temp: ValueObject; output_temp: ValueObject;
amp_hour: ValueObject; amp_hour: ValueObject;
} }

View File

@ -1,7 +1,7 @@
export interface HuaweiLimitConfig { export interface HuaweiLimitConfig {
voltage: number; voltage: number;
voltage_valid: boolean; voltage_valid: boolean;
current: number; current: number;
current_valid: boolean; current_valid: boolean;
online: boolean; online: boolean;
} }

View File

@ -55,21 +55,21 @@ export interface Vedirect {
} }
export interface Huawei { export interface Huawei {
enabled: boolean; enabled: boolean;
Power: ValueObject; Power: ValueObject;
} }
export interface Battery { export interface Battery {
enabled: boolean; enabled: boolean;
soc?: ValueObject; soc?: ValueObject;
voltage?: ValueObject; voltage?: ValueObject;
power?: ValueObject; power?: ValueObject;
current?: ValueObject; current?: ValueObject;
} }
export interface PowerMeter { export interface PowerMeter {
enabled: boolean; enabled: boolean;
Power: ValueObject; Power: ValueObject;
} }
export interface LiveData { export interface LiveData {

View File

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

View File

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

View File

@ -6,9 +6,12 @@
<form @submit="saveChargerConfig"> <form @submit="saveChargerConfig">
<CardElement :text="$t('acchargeradmin.Configuration')" textVariant="text-bg-primary"> <CardElement :text="$t('acchargeradmin.Configuration')" textVariant="text-bg-primary">
<InputElement :label="$t('acchargeradmin.EnableHuawei')" <InputElement
v-model="acChargerConfigList.enabled" :label="$t('acchargeradmin.EnableHuawei')"
type="checkbox" wide/> v-model="acChargerConfigList.enabled"
type="checkbox"
wide
/>
<div class="row mb-3" v-show="acChargerConfigList.enabled"> <div class="row mb-3" v-show="acChargerConfigList.enabled">
<label class="col-sm-4 col-form-label"> <label class="col-sm-4 col-form-label">
@ -16,123 +19,206 @@
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<select class="form-select" v-model="acChargerConfigList.can_controller_frequency"> <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 {{ frequency.key }} MHz
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<InputElement v-show="acChargerConfigList.enabled" <InputElement
:label="$t('acchargeradmin.VerboseLogging')" v-show="acChargerConfigList.enabled"
v-model="acChargerConfigList.verbose_logging" :label="$t('acchargeradmin.VerboseLogging')"
type="checkbox" wide/> v-model="acChargerConfigList.verbose_logging"
type="checkbox"
wide
/>
<InputElement v-show="acChargerConfigList.enabled" <InputElement
:label="$t('acchargeradmin.EnableAutoPower')" v-show="acChargerConfigList.enabled"
v-model="acChargerConfigList.auto_power_enabled" :label="$t('acchargeradmin.EnableAutoPower')"
type="checkbox" wide/> v-model="acChargerConfigList.auto_power_enabled"
type="checkbox"
wide
/>
<InputElement v-show="acChargerConfigList.enabled && acChargerConfigList.auto_power_enabled" <InputElement
:label="$t('acchargeradmin.EnableBatterySoCLimits')" v-show="acChargerConfigList.enabled && acChargerConfigList.auto_power_enabled"
v-model="acChargerConfigList.auto_power_batterysoc_limits_enabled" :label="$t('acchargeradmin.EnableBatterySoCLimits')"
type="checkbox" wide /> v-model="acChargerConfigList.auto_power_batterysoc_limits_enabled"
type="checkbox"
wide
/>
<InputElement v-show="acChargerConfigList.enabled" <InputElement
:label="$t('acchargeradmin.EnableEmergencyCharge')" v-show="acChargerConfigList.enabled"
v-model="acChargerConfigList.emergency_charge_enabled" :label="$t('acchargeradmin.EnableEmergencyCharge')"
type="checkbox" wide/> v-model="acChargerConfigList.emergency_charge_enabled"
type="checkbox"
wide
/>
<CardElement :text="$t('acchargeradmin.Limits')" textVariant="text-bg-primary" add-space <CardElement
v-show="acChargerConfigList.auto_power_enabled || acChargerConfigList.emergency_charge_enabled"> :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"> <div class="row mb-3">
<label for="voltageLimit" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.VoltageLimit') }}: <label for="voltageLimit" class="col-sm-2 col-form-label"
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.stopVoltageLimitHint')" /> >{{ $t('acchargeradmin.VoltageLimit') }}:
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.stopVoltageLimitHint')" />
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="number" step="0.01" class="form-control" id="voltageLimit" <input
placeholder="42" v-model="acChargerConfigList.voltage_limit" type="number"
aria-describedby="voltageLimitDescription" min="42" max="58.5" required/> step="0.01"
<span class="input-group-text" id="voltageLimitDescription">V</span> 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>
</div> </div>
<label for="enableVoltageLimit" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.enableVoltageLimit') }}: <label for="enableVoltageLimit" class="col-sm-2 col-form-label"
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.enableVoltageLimitHint')" /> >{{ $t('acchargeradmin.enableVoltageLimit') }}:
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.enableVoltageLimitHint')" />
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="number" step="0.01" class="form-control" id="enableVoltageLimit" <input
placeholder="42" v-model="acChargerConfigList.enable_voltage_limit" type="number"
aria-describedby="enableVoltageLimitDescription" min="42" max="58.5" required/> step="0.01"
<span class="input-group-text" id="enableVoltageLimitDescription">V</span> 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>
</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="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" id="lowerPowerLimit" <input
placeholder="150" v-model="acChargerConfigList.lower_power_limit" type="number"
aria-describedby="lowerPowerLimitDescription" min="50" max="3000" required/> class="form-control"
<span class="input-group-text" id="lowerPowerLimitDescription">W</span> 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>
</div> </div>
<label for="upperPowerLimit" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.upperPowerLimit') }}: <label for="upperPowerLimit" class="col-sm-2 col-form-label"
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.upperPowerLimitHint')" /> >{{ $t('acchargeradmin.upperPowerLimit') }}:
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.upperPowerLimitHint')" />
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" id="upperPowerLimit" <input
placeholder="2000" v-model="acChargerConfigList.upper_power_limit" type="number"
aria-describedby="upperPowerLimitDescription" min="100" max="3000" required/> class="form-control"
<span class="input-group-text" id="upperPowerLimitDescription">W</span> 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>
</div> </div>
<label for="targetPowerConsumption" class="col-sm-2 col-form-label">{{ $t('acchargeradmin.targetPowerConsumption') }}: <label for="targetPowerConsumption" class="col-sm-2 col-form-label"
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.targetPowerConsumptionHint')" /> >{{ $t('acchargeradmin.targetPowerConsumption') }}:
<BIconInfoCircle v-tooltip :title="$t('acchargeradmin.targetPowerConsumptionHint')" />
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" id="targetPowerConsumption" <input
placeholder="0" v-model="acChargerConfigList.target_power_consumption" type="number"
aria-describedby="targetPowerConsumptionDescription" min="-3000" max="3000" required/> class="form-control"
<span class="input-group-text" id="targetPowerConsumptionDescription">W</span> 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> </div>
</div> </div>
</CardElement> </CardElement>
<CardElement :text="$t('acchargeradmin.BatterySoCLimits')" textVariant="text-bg-primary" add-space <CardElement
v-show="acChargerConfigList.auto_power_enabled && acChargerConfigList.auto_power_batterysoc_limits_enabled"> :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"> <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')" /> <BIconInfoCircle v-tooltip :title="$t('acchargeradmin.StopBatterySoCThresholdHint')" />
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" id="stopBatterySoCThreshold" <input
placeholder="95" v-model="acChargerConfigList.stop_batterysoc_threshold" type="number"
aria-describedby="stopBatterySoCThresholdDescription" min="2" max="99" required/> class="form-control"
<span class="input-group-text" id="stopBatterySoCThresholdDescription">%</span> 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> </div>
</div> </div>
</CardElement> </CardElement>
</CardElement> </CardElement>
<FormFooter @reload="getChargerConfig"/> <FormFooter @reload="getChargerConfig" />
</form> </form>
</BasePage> </BasePage>
</template> </template>
<script lang="ts"> <script lang="ts">
import BasePage from '@/components/BasePage.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 CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import { BIconInfoCircle } from 'bootstrap-icons-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 { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -149,11 +235,11 @@ export default defineComponent({
return { return {
dataLoading: true, dataLoading: true,
acChargerConfigList: {} as AcChargerConfig, acChargerConfigList: {} as AcChargerConfig,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
frequencyTypeList: [ frequencyTypeList: [
{ key: 8, value: 8000000 }, { key: 8, value: 8000000 },
{ key: 16, value: 16000000 }, { key: 16, value: 16000000 },
], ],
}; };
@ -164,7 +250,7 @@ export default defineComponent({
methods: { methods: {
getChargerConfig() { getChargerConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/huawei/config", { headers: authHeader() }) fetch('/api/huawei/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.acChargerConfigList = data; this.acChargerConfigList = data;
@ -175,21 +261,19 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.acChargerConfigList)); formData.append('data', JSON.stringify(this.acChargerConfigList));
fetch("/api/huawei/config", { fetch('/api/huawei/config', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((response) => {
(response) => { this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
this.alertMessage = this.$t('apiresponse.' + response.code, response.param); this.alertType = response.type;
this.alertType = response.type; this.showAlert = true;
this.showAlert = true; });
}
);
}, },
}, },
}); });

View File

@ -6,14 +6,18 @@
<form @submit="saveBatteryConfig"> <form @submit="saveBatteryConfig">
<CardElement :text="$t('batteryadmin.BatteryConfiguration')" textVariant="text-bg-primary"> <CardElement :text="$t('batteryadmin.BatteryConfiguration')" textVariant="text-bg-primary">
<InputElement :label="$t('batteryadmin.EnableBattery')" <InputElement
v-model="batteryConfigList.enabled" :label="$t('batteryadmin.EnableBattery')"
type="checkbox" /> v-model="batteryConfigList.enabled"
type="checkbox"
/>
<InputElement v-show="batteryConfigList.enabled" <InputElement
:label="$t('batteryadmin.VerboseLogging')" v-show="batteryConfigList.enabled"
v-model="batteryConfigList.verbose_logging" :label="$t('batteryadmin.VerboseLogging')"
type="checkbox"/> v-model="batteryConfigList.verbose_logging"
type="checkbox"
/>
<div class="row mb-3" v-show="batteryConfigList.enabled"> <div class="row mb-3" v-show="batteryConfigList.enabled">
<label class="col-sm-2 col-form-label"> <label class="col-sm-2 col-form-label">
@ -29,61 +33,84 @@
</div> </div>
</CardElement> </CardElement>
<CardElement v-show="batteryConfigList.enabled && batteryConfigList.provider == 1" <CardElement
:text="$t('batteryadmin.JkBmsConfiguration')" textVariant="text-bg-primary" addSpace> v-show="batteryConfigList.enabled && batteryConfigList.provider == 1"
:text="$t('batteryadmin.JkBmsConfiguration')"
textVariant="text-bg-primary"
addSpace
>
<div class="row mb-3"> <div class="row mb-3">
<label class="col-sm-2 col-form-label"> <label class="col-sm-2 col-form-label">
{{ $t('batteryadmin.JkBmsInterface') }} {{ $t('batteryadmin.JkBmsInterface') }}
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-select" v-model="batteryConfigList.jkbms_interface"> <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) }} {{ $t(`batteryadmin.JkBmsInterface` + jkBmsInterface.value) }}
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<InputElement :label="$t('batteryadmin.PollingInterval')" <InputElement
v-model="batteryConfigList.jkbms_polling_interval" :label="$t('batteryadmin.PollingInterval')"
type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/> v-model="batteryConfigList.jkbms_polling_interval"
type="number"
min="2"
max="90"
step="1"
:postfix="$t('batteryadmin.Seconds')"
/>
</CardElement> </CardElement>
<template v-if="batteryConfigList.enabled && batteryConfigList.provider == 2"> <template v-if="batteryConfigList.enabled && batteryConfigList.provider == 2">
<CardElement :text="$t('batteryadmin.MqttSocConfiguration')" textVariant="text-bg-primary" addSpace> <CardElement :text="$t('batteryadmin.MqttSocConfiguration')" textVariant="text-bg-primary" addSpace>
<InputElement
<InputElement :label="$t('batteryadmin.MqttSocTopic')" :label="$t('batteryadmin.MqttSocTopic')"
v-model="batteryConfigList.mqtt_soc_topic" v-model="batteryConfigList.mqtt_soc_topic"
type="text" type="text"
maxlength="256" /> maxlength="256"
/>
<InputElement :label="$t('batteryadmin.MqttJsonPath')" <InputElement
:label="$t('batteryadmin.MqttJsonPath')"
v-model="batteryConfigList.mqtt_soc_json_path" v-model="batteryConfigList.mqtt_soc_json_path"
type="text" type="text"
maxlength="128" maxlength="128"
:tooltip="$t('batteryadmin.MqttJsonPathDescription')" /> :tooltip="$t('batteryadmin.MqttJsonPathDescription')"
/>
</CardElement> </CardElement>
<CardElement :text="$t('batteryadmin.MqttVoltageConfiguration')" textVariant="text-bg-primary" addSpace> <CardElement :text="$t('batteryadmin.MqttVoltageConfiguration')" textVariant="text-bg-primary" addSpace>
<InputElement
<InputElement :label="$t('batteryadmin.MqttVoltageTopic')" :label="$t('batteryadmin.MqttVoltageTopic')"
v-model="batteryConfigList.mqtt_voltage_topic" v-model="batteryConfigList.mqtt_voltage_topic"
type="text" type="text"
maxlength="256" /> maxlength="256"
/>
<InputElement :label="$t('batteryadmin.MqttJsonPath')" <InputElement
:label="$t('batteryadmin.MqttJsonPath')"
v-model="batteryConfigList.mqtt_voltage_json_path" v-model="batteryConfigList.mqtt_voltage_json_path"
type="text" type="text"
maxlength="128" maxlength="128"
:tooltip="$t('batteryadmin.MqttJsonPathDescription')" /> :tooltip="$t('batteryadmin.MqttJsonPathDescription')"
/>
<div class="row mb-3"> <div class="row mb-3">
<label for="mqtt_voltage_unit" class="col-sm-2 col-form-label"> <label for="mqtt_voltage_unit" class="col-sm-2 col-form-label">
{{ $t('batteryadmin.MqttVoltageUnit') }} {{ $t('batteryadmin.MqttVoltageUnit') }}
</label> </label>
<div class="col-sm-10"> <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"> <option v-for="u in voltageUnitTypeList" :key="u.key" :value="u.key">
{{ u.value }} {{ u.value }}
</option> </option>
@ -93,18 +120,18 @@
</CardElement> </CardElement>
</template> </template>
<FormFooter @reload="getBatteryConfig"/> <FormFooter @reload="getBatteryConfig" />
</form> </form>
</BasePage> </BasePage>
</template> </template>
<script lang="ts"> <script lang="ts">
import BasePage from '@/components/BasePage.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 CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.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 { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -120,8 +147,8 @@ export default defineComponent({
return { return {
dataLoading: true, dataLoading: true,
batteryConfigList: {} as BatteryConfig, batteryConfigList: {} as BatteryConfig,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
providerTypeList: [ providerTypeList: [
{ key: 0, value: 'PylontechCan' }, { key: 0, value: 'PylontechCan' },
@ -135,10 +162,10 @@ export default defineComponent({
{ key: 1, value: 'Transceiver' }, { key: 1, value: 'Transceiver' },
], ],
voltageUnitTypeList: [ voltageUnitTypeList: [
{ key: 3, value: "mV" }, { key: 3, value: 'mV' },
{ key: 2, value: "cV" }, { key: 2, value: 'cV' },
{ key: 1, value: "dV" }, { key: 1, value: 'dV' },
{ key: 0, value: "V" }, { key: 0, value: 'V' },
], ],
}; };
}, },
@ -148,7 +175,7 @@ export default defineComponent({
methods: { methods: {
getBatteryConfig() { getBatteryConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/battery/config", { headers: authHeader() }) fetch('/api/battery/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.batteryConfigList = data; this.batteryConfigList = data;
@ -159,21 +186,19 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.batteryConfigList)); formData.append('data', JSON.stringify(this.batteryConfigList));
fetch("/api/battery/config", { fetch('/api/battery/config', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((response) => {
(response) => { this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
this.alertMessage = this.$t('apiresponse.' + response.code, response.param); this.alertType = response.type;
this.alertType = response.type; this.showAlert = true;
this.showAlert = true; });
}
);
}, },
}, },
}); });

View File

@ -24,9 +24,11 @@
:postfix="$t('dtuadmin.Seconds')" :postfix="$t('dtuadmin.Seconds')"
/> />
<InputElement :label="$t('dtuadmin.VerboseLogging')" <InputElement
v-model="dtuConfigList.verbose_logging" :label="$t('dtuadmin.VerboseLogging')"
type="checkbox"/> v-model="dtuConfigList.verbose_logging"
type="checkbox"
/>
<div class="row mb-3" v-if="dtuConfigList.nrf_enabled"> <div class="row mb-3" v-if="dtuConfigList.nrf_enabled">
<label for="inputNrfPaLevel" class="col-sm-2 col-form-label"> <label for="inputNrfPaLevel" class="col-sm-2 col-form-label">

View File

@ -8,7 +8,13 @@
@reload="reloadData" @reload="reloadData"
> >
<HintView :hints="liveData.hints" /> <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="row gy-3 mt-0">
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { display: 'none' } : {}]"> <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"> <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 InverterTotalInfo from '@/components/InverterTotalInfo.vue';
import ModalDialog from '@/components/ModalDialog.vue'; import ModalDialog from '@/components/ModalDialog.vue';
import VedirectView from '@/components/VedirectView.vue'; import VedirectView from '@/components/VedirectView.vue';
import HuaweiView from '@/components/HuaweiView.vue' import HuaweiView from '@/components/HuaweiView.vue';
import BatteryView from '@/components/BatteryView.vue' import BatteryView from '@/components/BatteryView.vue';
import type { DevInfoStatus } from '@/types/DevInfoStatus'; import type { DevInfoStatus } from '@/types/DevInfoStatus';
import type { EventlogItems } from '@/types/EventlogStatus'; import type { EventlogItems } from '@/types/EventlogStatus';
import type { GridProfileStatus } from '@/types/GridProfileStatus'; import type { GridProfileStatus } from '@/types/GridProfileStatus';
@ -433,7 +439,7 @@ export default defineComponent({
BIconXCircleFill, BIconXCircleFill,
VedirectView, VedirectView,
HuaweiView, HuaweiView,
BatteryView BatteryView,
}, },
data() { data() {
return { return {
@ -577,12 +583,22 @@ export default defineComponent({
if (event.data != '{}') { if (event.data != '{}') {
const newData = JSON.parse(event.data); const newData = JSON.parse(event.data);
if (typeof newData.vedirect !== 'undefined') { Object.assign(this.liveData.vedirect, newData.vedirect); } if (typeof newData.vedirect !== 'undefined') {
if (typeof newData.huawei !== 'undefined') { Object.assign(this.liveData.huawei, newData.huawei); } Object.assign(this.liveData.vedirect, newData.vedirect);
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.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.total, newData.total);
Object.assign(this.liveData.hints, newData.hints); Object.assign(this.liveData.hints, newData.hints);

View File

@ -13,10 +13,13 @@
wide wide
/> />
<InputElement v-show="mqttConfigList.mqtt_enabled" <InputElement
:label="$t('mqttadmin.VerboseLogging')" v-show="mqttConfigList.mqtt_enabled"
v-model="mqttConfigList.mqtt_verbose_logging" :label="$t('mqttadmin.VerboseLogging')"
type="checkbox" wide/> v-model="mqttConfigList.mqtt_verbose_logging"
type="checkbox"
wide
/>
<InputElement <InputElement
v-show="mqttConfigList.mqtt_enabled" v-show="mqttConfigList.mqtt_enabled"

View File

@ -22,7 +22,11 @@
<tr> <tr>
<th>{{ $t('mqttinfo.VerboseLogging') }}</th> <th>{{ $t('mqttinfo.VerboseLogging') }}</th>
<td> <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> </td>
</tr> </tr>
<tr> <tr>

View File

@ -8,13 +8,19 @@
{{ $t('powerlimiteradmin.ConfigAlertMessage') }} {{ $t('powerlimiteradmin.ConfigAlertMessage') }}
</BootstrapAlert> </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="row">
<div class="col-sm-12"> <div class="col-sm-12">
{{ $t('powerlimiteradmin.ConfigHintsIntro') }} {{ $t('powerlimiteradmin.ConfigHintsIntro') }}
<ul class="mb-0"> <ul class="mb-0">
<li v-for="(hint, idx) in getConfigHints()" :key="idx"> <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> <b v-if="hint.severity === 'optional'">{{ $t('powerlimiteradmin.ConfigHintOptional') }}:</b>
{{ $t('powerlimiteradmin.ConfigHint' + hint.subject) }} {{ $t('powerlimiteradmin.ConfigHint' + hint.subject) }}
</li> </li>
@ -25,91 +31,153 @@
<form @submit="savePowerLimiterConfig" v-if="!configAlert"> <form @submit="savePowerLimiterConfig" v-if="!configAlert">
<CardElement :text="$t('powerlimiteradmin.General')" textVariant="text-bg-primary" add-space> <CardElement :text="$t('powerlimiteradmin.General')" textVariant="text-bg-primary" add-space>
<InputElement :label="$t('powerlimiteradmin.Enable')" <InputElement
v-model="powerLimiterConfigList.enabled" :label="$t('powerlimiteradmin.Enable')"
type="checkbox" wide/> v-model="powerLimiterConfigList.enabled"
type="checkbox"
wide
/>
<InputElement v-show="isEnabled()" <InputElement
:label="$t('powerlimiteradmin.VerboseLogging')" v-show="isEnabled()"
v-model="powerLimiterConfigList.verbose_logging" :label="$t('powerlimiteradmin.VerboseLogging')"
type="checkbox" wide/> v-model="powerLimiterConfigList.verbose_logging"
type="checkbox"
wide
/>
<InputElement v-show="isEnabled() && hasPowerMeter()" <InputElement
:label="$t('powerlimiteradmin.TargetPowerConsumption')" v-show="isEnabled() && hasPowerMeter()"
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHint')" :label="$t('powerlimiteradmin.TargetPowerConsumption')"
v-model="powerLimiterConfigList.target_power_consumption" :tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHint')"
postfix="W" v-model="powerLimiterConfigList.target_power_consumption"
type="number" wide/> postfix="W"
type="number"
wide
/>
<InputElement v-show="isEnabled()" <InputElement
:label="$t('powerlimiteradmin.TargetPowerConsumptionHysteresis')" v-show="isEnabled()"
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHysteresisHint')" :label="$t('powerlimiteradmin.TargetPowerConsumptionHysteresis')"
v-model="powerLimiterConfigList.target_power_consumption_hysteresis" :tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHysteresisHint')"
postfix="W" v-model="powerLimiterConfigList.target_power_consumption_hysteresis"
type="number" wide/> postfix="W"
type="number"
wide
/>
</CardElement> </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"> <div class="row mb-3">
<label for="inverter_serial" class="col-sm-4 col-form-label"> <label for="inverter_serial" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.Inverter') }} {{ $t('powerlimiteradmin.Inverter') }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="inverter_serial" class="form-select" v-model="powerLimiterConfigList.inverter_serial" required> <select
<option value="" disabled hidden selected>{{ $t('powerlimiteradmin.SelectInverter') }}</option> id="inverter_serial"
<option v-for="(inv, serial) in powerLimiterMetaData.inverters" :key="serial" :value="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 }}) {{ inv.name }} ({{ inv.type }})
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<InputElement :label="$t('powerlimiteradmin.InverterIsSolarPowered')" <InputElement
v-model="powerLimiterConfigList.is_inverter_solar_powered" :label="$t('powerlimiteradmin.InverterIsSolarPowered')"
type="checkbox" wide/> v-model="powerLimiterConfigList.is_inverter_solar_powered"
type="checkbox"
wide
/>
<InputElement v-show="canUseOverscaling()" <InputElement
:label="$t('powerlimiteradmin.UseOverscalingToCompensateShading')" v-show="canUseOverscaling()"
:tooltip="$t('powerlimiteradmin.UseOverscalingToCompensateShadingHint')" :label="$t('powerlimiteradmin.UseOverscalingToCompensateShading')"
v-model="powerLimiterConfigList.use_overscaling_to_compensate_shading" :tooltip="$t('powerlimiteradmin.UseOverscalingToCompensateShadingHint')"
type="checkbox" wide/> v-model="powerLimiterConfigList.use_overscaling_to_compensate_shading"
type="checkbox"
wide
/>
<div class="row mb-3" v-if="needsChannelSelection()"> <div class="row mb-3" v-if="needsChannelSelection()">
<label for="inverter_channel" class="col-sm-4 col-form-label"> <label for="inverter_channel" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.InverterChannelId') }} {{ $t('powerlimiteradmin.InverterChannelId') }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="inverter_channel" class="form-select" v-model="powerLimiterConfigList.inverter_channel_id"> <select
<option v-for="channel in range(powerLimiterMetaData.inverters[powerLimiterConfigList.inverter_serial].channels)" :key="channel" :value="channel"> 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 }} {{ channel + 1 }}
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<InputElement :label="$t('powerlimiteradmin.LowerPowerLimit')" <InputElement
:tooltip="$t('powerlimiteradmin.LowerPowerLimitHint')" :label="$t('powerlimiteradmin.LowerPowerLimit')"
v-model="powerLimiterConfigList.lower_power_limit" :tooltip="$t('powerlimiteradmin.LowerPowerLimitHint')"
placeholder="50" min="10" postfix="W" v-model="powerLimiterConfigList.lower_power_limit"
type="number" wide/> placeholder="50"
min="10"
postfix="W"
type="number"
wide
/>
<InputElement :label="$t('powerlimiteradmin.BaseLoadLimit')" <InputElement
:tooltip="$t('powerlimiteradmin.BaseLoadLimitHint')" :label="$t('powerlimiteradmin.BaseLoadLimit')"
v-model="powerLimiterConfigList.base_load_limit" :tooltip="$t('powerlimiteradmin.BaseLoadLimitHint')"
placeholder="200" :min="(powerLimiterConfigList.lower_power_limit + 1).toString()" postfix="W" v-model="powerLimiterConfigList.base_load_limit"
type="number" wide/> placeholder="200"
:min="(powerLimiterConfigList.lower_power_limit + 1).toString()"
postfix="W"
type="number"
wide
/>
<InputElement :label="$t('powerlimiteradmin.UpperPowerLimit')" <InputElement
v-model="powerLimiterConfigList.upper_power_limit" :label="$t('powerlimiteradmin.UpperPowerLimit')"
:tooltip="$t('powerlimiteradmin.UpperPowerLimitHint')" v-model="powerLimiterConfigList.upper_power_limit"
placeholder="800" :min="(powerLimiterConfigList.base_load_limit + 1).toString()" postfix="W" :tooltip="$t('powerlimiteradmin.UpperPowerLimitHint')"
type="number" wide/> placeholder="800"
:min="(powerLimiterConfigList.base_load_limit + 1).toString()"
postfix="W"
type="number"
wide
/>
<InputElement v-show="hasPowerMeter()" <InputElement
:label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')" v-show="hasPowerMeter()"
v-model="powerLimiterConfigList.is_inverter_behind_powermeter" :label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')"
:tooltip="$t('powerlimiteradmin.InverterIsBehindPowerMeterHint')" v-model="powerLimiterConfigList.is_inverter_behind_powermeter"
type="checkbox" wide/> :tooltip="$t('powerlimiteradmin.InverterIsBehindPowerMeterHint')"
type="checkbox"
wide
/>
<div class="row mb-3" v-if="!powerLimiterConfigList.is_inverter_solar_powered"> <div class="row mb-3" v-if="!powerLimiterConfigList.is_inverter_solar_powered">
<label for="inverter_restart" class="col-sm-4 col-form-label"> <label for="inverter_restart" class="col-sm-4 col-form-label">
@ -117,100 +185,200 @@
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterRestartHint')" /> <BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterRestartHint')" />
</label> </label>
<div class="col-sm-8"> <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"> <option value="-1">
{{ $t('powerlimiteradmin.InverterRestartDisabled') }} {{ $t('powerlimiteradmin.InverterRestartDisabled') }}
</option> </option>
<option v-for="hour in range(24)" :key="hour" :value="hour"> <option v-for="hour in range(24)" :key="hour" :value="hour">
{{ (hour > 9) ? hour : "0"+hour }}:00 {{ hour > 9 ? hour : '0' + hour }}:00
</option> </option>
</select> </select>
</div> </div>
</div> </div>
</CardElement> </CardElement>
<CardElement :text="$t('powerlimiteradmin.SolarPassthrough')" textVariant="text-bg-primary" add-space v-if="canUseSolarPassthrough()"> <CardElement
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.SolarpassthroughInfo')"></div> :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')" <InputElement
v-model="powerLimiterConfigList.solar_passthrough_enabled" :label="$t('powerlimiteradmin.EnableSolarPassthrough')"
type="checkbox" wide/> v-model="powerLimiterConfigList.solar_passthrough_enabled"
type="checkbox"
wide
/>
<div v-if="powerLimiterConfigList.solar_passthrough_enabled"> <div v-if="powerLimiterConfigList.solar_passthrough_enabled">
<InputElement :label="$t('powerlimiteradmin.BatteryDischargeAtNight')" <InputElement
v-model="powerLimiterConfigList.battery_always_use_at_night" :label="$t('powerlimiteradmin.BatteryDischargeAtNight')"
type="checkbox" wide/> v-model="powerLimiterConfigList.battery_always_use_at_night"
type="checkbox"
wide
/>
<InputElement :label="$t('powerlimiteradmin.SolarPassthroughLosses')" <InputElement
v-model="powerLimiterConfigList.solar_passthrough_losses" :label="$t('powerlimiteradmin.SolarPassthroughLosses')"
placeholder="3" min="0" max="10" postfix="%" v-model="powerLimiterConfigList.solar_passthrough_losses"
type="number" wide/> 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> </div>
</CardElement> </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 <InputElement
:label="$t('powerlimiteradmin.IgnoreSoc')" :label="$t('powerlimiteradmin.IgnoreSoc')"
v-model="powerLimiterConfigList.ignore_soc" v-model="powerLimiterConfigList.ignore_soc"
type="checkbox" wide/> type="checkbox"
wide
/>
<div v-if="!powerLimiterConfigList.ignore_soc"> <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')" <InputElement
v-model="powerLimiterConfigList.battery_soc_start_threshold" :label="$t('powerlimiteradmin.StartThreshold')"
placeholder="80" min="0" max="100" postfix="%" v-model="powerLimiterConfigList.battery_soc_start_threshold"
type="number" wide/> placeholder="80"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
<InputElement :label="$t('powerlimiteradmin.StopThreshold')" <InputElement
v-model="powerLimiterConfigList.battery_soc_stop_threshold" :label="$t('powerlimiteradmin.StopThreshold')"
placeholder="20" min="0" max="100" postfix="%" v-model="powerLimiterConfigList.battery_soc_stop_threshold"
type="number" wide/> placeholder="20"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
<InputElement :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')" <InputElement
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')" :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
v-model="powerLimiterConfigList.full_solar_passthrough_soc" :tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
v-if="isSolarPassthroughEnabled()" v-model="powerLimiterConfigList.full_solar_passthrough_soc"
placeholder="80" min="0" max="100" postfix="%" v-if="isSolarPassthroughEnabled()"
type="number" wide/> placeholder="80"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
</div> </div>
</CardElement> </CardElement>
<CardElement :text="$t('powerlimiteradmin.VoltageThresholds')" textVariant="text-bg-primary" add-space v-if="canUseVoltageThresholds()"> <CardElement
<InputElement :label="$t('powerlimiteradmin.StartThreshold')" :text="$t('powerlimiteradmin.VoltageThresholds')"
v-model="powerLimiterConfigList.voltage_start_threshold" textVariant="text-bg-primary"
placeholder="50" min="16" max="66" postfix="V" add-space
type="number" step="0.01" wide/> 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')" <InputElement
v-model="powerLimiterConfigList.voltage_stop_threshold" :label="$t('powerlimiteradmin.StopThreshold')"
placeholder="49" min="16" max="66" postfix="V" v-model="powerLimiterConfigList.voltage_stop_threshold"
type="number" step="0.01" wide/> placeholder="49"
min="16"
max="66"
postfix="V"
type="number"
step="0.01"
wide
/>
<div v-if="isSolarPassthroughEnabled()"> <div v-if="isSolarPassthroughEnabled()">
<InputElement :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')" <InputElement
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')" :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
v-model="powerLimiterConfigList.full_solar_passthrough_start_voltage" :tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
placeholder="49" min="16" max="66" postfix="V" v-model="powerLimiterConfigList.full_solar_passthrough_start_voltage"
type="number" step="0.01" wide/> placeholder="49"
min="16"
max="66"
postfix="V"
type="number"
step="0.01"
wide
/>
<InputElement :label="$t('powerlimiteradmin.VoltageSolarPassthroughStopThreshold')" <InputElement
v-model="powerLimiterConfigList.full_solar_passthrough_stop_voltage" :label="$t('powerlimiteradmin.VoltageSolarPassthroughStopThreshold')"
placeholder="49" min="16" max="66" postfix="V" v-model="powerLimiterConfigList.full_solar_passthrough_stop_voltage"
type="number" step="0.01" wide/> placeholder="49"
min="16"
max="66"
postfix="V"
type="number"
step="0.01"
wide
/>
</div> </div>
<InputElement :label="$t('powerlimiteradmin.VoltageLoadCorrectionFactor')" <InputElement
v-model="powerLimiterConfigList.voltage_load_correction_factor" :label="$t('powerlimiteradmin.VoltageLoadCorrectionFactor')"
placeholder="0.0001" postfix="1/A" v-model="powerLimiterConfigList.voltage_load_correction_factor"
type="number" step="0.0001" wide/> 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> </CardElement>
<FormFooter @reload="getAllData"/> <FormFooter @reload="getAllData" />
</form> </form>
</BasePage> </BasePage>
</template> </template>
@ -218,13 +386,13 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.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 { handleResponse, authHeader } from '@/utils/authentication';
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import { BIconInfoCircle } from 'bootstrap-icons-vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue';
import type { PowerLimiterConfig, PowerLimiterMetaData } from "@/types/PowerLimiterConfig"; import type { PowerLimiterConfig, PowerLimiterMetaData } from '@/types/PowerLimiterConfig';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -240,8 +408,8 @@ export default defineComponent({
dataLoading: true, dataLoading: true,
powerLimiterConfigList: {} as PowerLimiterConfig, powerLimiterConfigList: {} as PowerLimiterConfig,
powerLimiterMetaData: {} as PowerLimiterMetaData, powerLimiterMetaData: {} as PowerLimiterMetaData,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
configAlert: false, configAlert: false,
}; };
@ -254,9 +422,13 @@ export default defineComponent({
const cfg = this.powerLimiterConfigList; const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData; 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)) { for (const [serial, inverter] of Object.entries(meta.inverters)) {
// cfg.inverter_serial might be too large to parse as a 32 bit // 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 // previously selected inverter was deleted. marks serial as
// invalid, selects placeholder option. // invalid, selects placeholder option.
cfg.inverter_serial = ''; cfg.inverter_serial = '';
} },
}, },
methods: { methods: {
getConfigHints() { getConfigHints() {
@ -283,27 +455,29 @@ export default defineComponent({
const hints = []; const hints = [];
if (meta.power_meter_enabled !== true) { 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) { if (typeof meta.inverters === 'undefined' || Object.keys(meta.inverters).length == 0) {
hints.push({severity: "requirement", subject: "NoInverter"}); hints.push({ severity: 'requirement', subject: 'NoInverter' });
this.configAlert = true; this.configAlert = true;
} } else {
else {
const inv = meta.inverters[cfg.inverter_serial]; const inv = meta.inverters[cfg.inverter_serial];
if (inv !== undefined && !(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) { if (
hints.push({severity: "requirement", subject: "InverterCommunication"}); inv !== undefined &&
!(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)
) {
hints.push({ severity: 'requirement', subject: 'InverterCommunication' });
} }
} }
if (!cfg.is_inverter_solar_powered) { if (!cfg.is_inverter_solar_powered) {
if (!meta.charge_controller_enabled) { if (!meta.charge_controller_enabled) {
hints.push({severity: "optional", subject: "NoChargeController"}); hints.push({ severity: 'optional', subject: 'NoChargeController' });
} }
if (!meta.battery_enabled) { 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 cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData; const meta = this.powerLimiterMetaData;
const canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered; 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; return canUse;
}, },
canUseSoCThresholds() { canUseSoCThresholds() {
@ -345,17 +521,23 @@ export default defineComponent({
const cfg = this.powerLimiterConfigList; const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData; const meta = this.powerLimiterMetaData;
const reset = function() { const reset = function () {
cfg.inverter_channel_id = 0; cfg.inverter_channel_id = 0;
return false; 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]; const inverter = meta.inverters[cfg.inverter_serial];
if (inverter === undefined) { return reset(); } if (inverter === undefined) {
return reset();
}
if (cfg.inverter_channel_id >= inverter.channels) { if (cfg.inverter_channel_id >= inverter.channels) {
reset(); reset();
@ -365,11 +547,11 @@ export default defineComponent({
}, },
getAllData() { getAllData() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/powerlimiter/metadata", { headers: authHeader() }) fetch('/api/powerlimiter/metadata', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.powerLimiterMetaData = 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((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.powerLimiterConfigList = data; this.powerLimiterConfigList = data;
@ -381,21 +563,19 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.powerLimiterConfigList)); formData.append('data', JSON.stringify(this.powerLimiterConfigList));
fetch("/api/powerlimiter/config", { fetch('/api/powerlimiter/config', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((response) => {
(response) => { this.alertMessage = response.message;
this.alertMessage = response.message; this.alertType = response.type;
this.alertType = response.type; this.showAlert = true;
this.showAlert = true; });
}
);
}, },
}, },
}); });

View File

@ -5,20 +5,26 @@
</BootstrapAlert> </BootstrapAlert>
<form @submit="savePowerMeterConfig"> <form @submit="savePowerMeterConfig">
<CardElement :text="$t('powermeteradmin.PowerMeterConfiguration')" <CardElement :text="$t('powermeteradmin.PowerMeterConfiguration')" textVariant="text-bg-primary">
textVariant="text-bg-primary"> <InputElement
:label="$t('powermeteradmin.PowerMeterEnable')"
v-model="powerMeterConfigList.enabled"
type="checkbox"
wide
/>
<InputElement :label="$t('powermeteradmin.PowerMeterEnable')" <InputElement
v-model="powerMeterConfigList.enabled" v-show="powerMeterConfigList.enabled"
type="checkbox" wide /> :label="$t('powermeteradmin.VerboseLogging')"
v-model="powerMeterConfigList.verbose_logging"
<InputElement v-show="powerMeterConfigList.enabled" type="checkbox"
:label="$t('powermeteradmin.VerboseLogging')" wide
v-model="powerMeterConfigList.verbose_logging" />
type="checkbox" wide />
<div class="row mb-3" v-show="powerMeterConfigList.enabled"> <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"> <div class="col-sm-8">
<select id="inputPowerMeterSource" class="form-select" v-model="powerMeterConfigList.source"> <select id="inputPowerMeterSource" class="form-select" v-model="powerMeterConfigList.source">
<option v-for="source in powerMeterSourceList" :key="source.key" :value="source.key"> <option v-for="source in powerMeterSourceList" :key="source.key" :value="source.key">
@ -35,8 +41,20 @@
<h2>{{ $t('powermeteradmin.jsonPathExamplesHeading') }}:</h2> <h2>{{ $t('powermeteradmin.jsonPathExamplesHeading') }}:</h2>
{{ $t('powermeteradmin.jsonPathExamplesExplanation') }} {{ $t('powermeteradmin.jsonPathExamplesExplanation') }}
<ul> <ul>
<li><code>power/total/watts</code> &mdash; <code>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 } } }</code></li> <li>
<li><code>data/[1]/power</code> &mdash; <code>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] } }</code></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> <li><code>total</code> &mdash; <code>{ "othervalue": 66, "total": 123.4 }</code></li>
</ul> </ul>
</div> </div>
@ -45,23 +63,28 @@
<!-- yarn linter wants us to not combine v-if with v-for, so we need to wrap the CardElements //--> <!-- 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"> <div v-if="powerMeterConfigList.source === 0">
<CardElement <CardElement
v-for="(mqtt, index) in powerMeterConfigList.mqtt.values" v-bind:key="index" v-for="(mqtt, index) in powerMeterConfigList.mqtt.values"
:text="$t('powermeteradmin.MqttValue', { valueNumber: index + 1})" v-bind:key="index"
textVariant="text-bg-primary" :text="$t('powermeteradmin.MqttValue', { valueNumber: index + 1 })"
add-space> textVariant="text-bg-primary"
add-space
<InputElement :label="$t('powermeteradmin.MqttTopic')" >
<InputElement
:label="$t('powermeteradmin.MqttTopic')"
v-model="mqtt.topic" v-model="mqtt.topic"
type="text" type="text"
maxlength="256" maxlength="256"
wide /> wide
/>
<InputElement :label="$t('powermeteradmin.mqttJsonPath')" <InputElement
:label="$t('powermeteradmin.mqttJsonPath')"
v-model="mqtt.json_path" v-model="mqtt.json_path"
type="text" type="text"
maxlength="256" maxlength="256"
:tooltip="$t('powermeteradmin.valueJsonPathDescription')" :tooltip="$t('powermeteradmin.valueJsonPathDescription')"
wide /> wide
/>
<div class="row mb-3"> <div class="row mb-3">
<label for="mqtt_power_unit" class="col-sm-4 col-form-label"> <label for="mqtt_power_unit" class="col-sm-4 col-form-label">
@ -81,27 +104,33 @@
v-model="mqtt.sign_inverted" v-model="mqtt.sign_inverted"
:tooltip="$t('powermeteradmin.valueSignInvertedHint')" :tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox" type="checkbox"
wide /> wide
/>
</CardElement> </CardElement>
</div> </div>
<CardElement v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2)" <CardElement
:text="$t('powermeteradmin.SDM')" v-if="powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2"
textVariant="text-bg-primary" :text="$t('powermeteradmin.SDM')"
add-space> textVariant="text-bg-primary"
add-space
<InputElement :label="$t('powermeteradmin.pollingInterval')" >
<InputElement
:label="$t('powermeteradmin.pollingInterval')"
v-model="powerMeterConfigList.serial_sdm.polling_interval" v-model="powerMeterConfigList.serial_sdm.polling_interval"
type="number" type="number"
min=1 min="1"
max=15 max="15"
:postfix="$t('powermeteradmin.seconds')" :postfix="$t('powermeteradmin.seconds')"
wide /> wide
/>
<InputElement :label="$t('powermeteradmin.sdmaddress')" <InputElement
:label="$t('powermeteradmin.sdmaddress')"
v-model="powerMeterConfigList.serial_sdm.address" v-model="powerMeterConfigList.serial_sdm.address"
type="number" type="number"
wide /> wide
/>
</CardElement> </CardElement>
<div v-if="powerMeterConfigList.source === 3"> <div v-if="powerMeterConfigList.source === 3">
@ -115,45 +144,54 @@
</ul> </ul>
</div> </div>
<CardElement :text="$t('powermeteradmin.HTTP')" <CardElement :text="$t('powermeteradmin.HTTP')" textVariant="text-bg-primary" add-space>
textVariant="text-bg-primary" <InputElement
add-space> :label="$t('powermeteradmin.httpIndividualRequests')"
<InputElement :label="$t('powermeteradmin.httpIndividualRequests')"
v-model="powerMeterConfigList.http_json.individual_requests" v-model="powerMeterConfigList.http_json.individual_requests"
type="checkbox" type="checkbox"
wide /> wide
/>
<InputElement :label="$t('powermeteradmin.pollingInterval')" <InputElement
:label="$t('powermeteradmin.pollingInterval')"
v-model="powerMeterConfigList.http_json.polling_interval" v-model="powerMeterConfigList.http_json.polling_interval"
type="number" type="number"
min=1 min="1"
max=15 max="15"
:postfix="$t('powermeteradmin.seconds')" :postfix="$t('powermeteradmin.seconds')"
wide /> wide
/>
</CardElement> </CardElement>
<CardElement <CardElement
v-for="(httpJson, index) in powerMeterConfigList.http_json.values" v-for="(httpJson, index) in powerMeterConfigList.http_json.values"
:key="index" :key="index"
:text="$t('powermeteradmin.httpValue', { valueNumber: index + 1 })" :text="$t('powermeteradmin.httpValue', { valueNumber: index + 1 })"
textVariant="text-bg-primary" textVariant="text-bg-primary"
add-space> add-space
>
<InputElement <InputElement
v-if="index > 0" v-if="index > 0"
:label="$t('powermeteradmin.httpEnabled')" :label="$t('powermeteradmin.httpEnabled')"
v-model="httpJson.enabled" v-model="httpJson.enabled"
type="checkbox" wide /> type="checkbox"
wide
/>
<div v-if="httpJson.enabled || index == 0"> <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" v-model="httpJson.json_path"
type="text" type="text"
maxlength="256" maxlength="256"
:tooltip="$t('powermeteradmin.valueJsonPathDescription')" :tooltip="$t('powermeteradmin.valueJsonPathDescription')"
wide /> wide
/>
<div class="row mb-3"> <div class="row mb-3">
<label for="power_unit" class="col-sm-4 col-form-label"> <label for="power_unit" class="col-sm-4 col-form-label">
@ -173,63 +211,70 @@
v-model="httpJson.sign_inverted" v-model="httpJson.sign_inverted"
:tooltip="$t('powermeteradmin.valueSignInvertedHint')" :tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox" type="checkbox"
wide /> wide
/>
</div> </div>
</CardElement> </CardElement>
<CardElement <CardElement
:text="$t('powermeteradmin.testHttpJsonHeader')" :text="$t('powermeteradmin.testHttpJsonHeader')"
textVariant="text-bg-primary" textVariant="text-bg-primary"
add-space> add-space
>
<div class="text-center mt-3 mb-3"> <div class="text-center mt-3 mb-3">
<button type="button" class="btn btn-primary" @click="testHttpJsonRequest()"> <button type="button" class="btn btn-primary" @click="testHttpJsonRequest()">
{{ $t('powermeteradmin.testHttpJsonRequest') }} {{ $t('powermeteradmin.testHttpJsonRequest') }}
</button> </button>
</div> </div>
<BootstrapAlert v-model="testHttpJsonRequestAlert.show" dismissible :variant="testHttpJsonRequestAlert.type"> <BootstrapAlert
v-model="testHttpJsonRequestAlert.show"
dismissible
:variant="testHttpJsonRequestAlert.type"
>
{{ testHttpJsonRequestAlert.message }} {{ testHttpJsonRequestAlert.message }}
</BootstrapAlert> </BootstrapAlert>
</CardElement> </CardElement>
</div> </div>
<div v-if="powerMeterConfigList.source === 6"> <div v-if="powerMeterConfigList.source === 6">
<CardElement :text="$t('powermeteradmin.HTTP_SML')" <CardElement :text="$t('powermeteradmin.HTTP_SML')" textVariant="text-bg-primary" add-space>
textVariant="text-bg-primary" <InputElement
add-space> :label="$t('powermeteradmin.pollingInterval')"
<InputElement :label="$t('powermeteradmin.pollingInterval')"
v-model="powerMeterConfigList.http_sml.polling_interval" v-model="powerMeterConfigList.http_sml.polling_interval"
type="number" type="number"
min=1 min="1"
max=15 max="15"
:postfix="$t('powermeteradmin.seconds')" :postfix="$t('powermeteradmin.seconds')"
wide /> wide
/>
<HttpRequestSettings v-model="powerMeterConfigList.http_sml.http_request" /> <HttpRequestSettings v-model="powerMeterConfigList.http_sml.http_request" />
</CardElement> </CardElement>
<CardElement <CardElement
:text="$t('powermeteradmin.testHttpSmlHeader')" :text="$t('powermeteradmin.testHttpSmlHeader')"
textVariant="text-bg-primary" textVariant="text-bg-primary"
add-space> 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"> <BootstrapAlert
<button type="button" class="btn btn-primary" @click="testHttpSmlRequest()"> v-model="testHttpSmlRequestAlert.show"
{{ $t('powermeteradmin.testHttpSmlRequest') }} dismissible
</button> :variant="testHttpSmlRequestAlert.type"
</div> >
{{ testHttpSmlRequestAlert.message }}
<BootstrapAlert v-model="testHttpSmlRequestAlert.show" dismissible :variant="testHttpSmlRequestAlert.type"> </BootstrapAlert>
{{ testHttpSmlRequestAlert.message }}
</BootstrapAlert>
</CardElement> </CardElement>
</div> </div>
</div> </div>
<FormFooter @reload="getPowerMeterConfig"/> <FormFooter @reload="getPowerMeterConfig" />
</form> </form>
</BasePage> </BasePage>
</template> </template>
@ -237,13 +282,13 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.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 CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import HttpRequestSettings from '@/components/HttpRequestSettings.vue'; import HttpRequestSettings from '@/components/HttpRequestSettings.vue';
import { handleResponse, authHeader } from '@/utils/authentication'; import { handleResponse, authHeader } from '@/utils/authentication';
import type { PowerMeterConfig } from "@/types/PowerMeterConfig"; import type { PowerMeterConfig } from '@/types/PowerMeterConfig';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -252,7 +297,7 @@ export default defineComponent({
CardElement, CardElement,
FormFooter, FormFooter,
HttpRequestSettings, HttpRequestSettings,
InputElement InputElement,
}, },
data() { data() {
return { return {
@ -268,15 +313,23 @@ export default defineComponent({
{ key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') }, { key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') },
], ],
unitTypeList: [ unitTypeList: [
{ key: 1, value: "mW" }, { key: 1, value: 'mW' },
{ key: 0, value: "W" }, { key: 0, value: 'W' },
{ key: 2, value: "kW" }, { key: 2, value: 'kW' },
], ],
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
testHttpJsonRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; }, testHttpJsonRequestAlert: { message: '', type: '', show: false } as {
testHttpSmlRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; } message: string;
type: string;
show: boolean;
},
testHttpSmlRequestAlert: { message: '', type: '', show: false } as {
message: string;
type: string;
show: boolean;
},
}; };
}, },
created() { created() {
@ -285,7 +338,7 @@ export default defineComponent({
methods: { methods: {
getPowerMeterConfig() { getPowerMeterConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/powermeter/config", { headers: authHeader() }) fetch('/api/powermeter/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.powerMeterConfigList = data; this.powerMeterConfigList = data;
@ -296,74 +349,68 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.powerMeterConfigList)); formData.append('data', JSON.stringify(this.powerMeterConfigList));
fetch("/api/powermeter/config", { fetch('/api/powermeter/config', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((response) => {
(response) => { this.alertMessage = response.message;
this.alertMessage = response.message; this.alertType = response.type;
this.alertType = response.type; this.showAlert = true;
this.showAlert = true; window.scrollTo(0, 0);
window.scrollTo(0, 0); });
}
);
}, },
testHttpJsonRequest() { testHttpJsonRequest() {
this.testHttpJsonRequestAlert = { this.testHttpJsonRequestAlert = {
message: "Triggering HTTP request...", message: 'Triggering HTTP request...',
type: "info", type: 'info',
show: true, show: true,
}; };
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.powerMeterConfigList)); formData.append('data', JSON.stringify(this.powerMeterConfigList));
fetch("/api/powermeter/testhttpjsonrequest", { fetch('/api/powermeter/testhttpjsonrequest', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((response) => {
(response) => { this.testHttpJsonRequestAlert = {
this.testHttpJsonRequestAlert = { message: response.message,
message: response.message, type: response.type,
type: response.type, show: true,
show: true, };
}; });
}
)
}, },
testHttpSmlRequest() { testHttpSmlRequest() {
this.testHttpSmlRequestAlert = { this.testHttpSmlRequestAlert = {
message: "Triggering HTTP request...", message: 'Triggering HTTP request...',
type: "info", type: 'info',
show: true, show: true,
}; };
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.powerMeterConfigList)); formData.append('data', JSON.stringify(this.powerMeterConfigList));
fetch("/api/powermeter/testhttpsmlrequest", { fetch('/api/powermeter/testhttpsmlrequest', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((response) => {
(response) => { this.testHttpSmlRequestAlert = {
this.testHttpSmlRequestAlert = { message: response.message,
message: response.message, type: response.type,
type: response.type, show: true,
show: true, };
}; });
}
)
}, },
}, },
}); });

View File

@ -77,7 +77,9 @@ export default defineComponent({
} }
const fetchUrl = 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) fetch(fetchUrl)
.then((response) => { .then((response) => {

View File

@ -6,35 +6,47 @@
<form @submit="saveVedirectConfig"> <form @submit="saveVedirectConfig">
<CardElement :text="$t('vedirectadmin.VedirectConfiguration')" textVariant="text-bg-primary"> <CardElement :text="$t('vedirectadmin.VedirectConfiguration')" textVariant="text-bg-primary">
<InputElement :label="$t('vedirectadmin.EnableVedirect')" <InputElement
v-model="vedirectConfigList.vedirect_enabled" :label="$t('vedirectadmin.EnableVedirect')"
type="checkbox" wide/> v-model="vedirectConfigList.vedirect_enabled"
type="checkbox"
wide
/>
</CardElement> </CardElement>
<CardElement :text="$t('vedirectadmin.VedirectParameter')" textVariant="text-bg-primary" add-space <CardElement
v-show="vedirectConfigList.vedirect_enabled"> :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')" <InputElement
v-model="vedirectConfigList.verbose_logging" :label="$t('vedirectadmin.UpdatesOnly')"
type="checkbox" wide/> v-model="vedirectConfigList.vedirect_updatesonly"
type="checkbox"
<InputElement :label="$t('vedirectadmin.UpdatesOnly')" wide
v-model="vedirectConfigList.vedirect_updatesonly" />
type="checkbox" wide/>
</CardElement> </CardElement>
<FormFooter @reload="getVedirectConfig"/> <FormFooter @reload="getVedirectConfig" />
</form> </form>
</BasePage> </BasePage>
</template> </template>
<script lang="ts"> <script lang="ts">
import BasePage from '@/components/BasePage.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 CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.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 { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -50,8 +62,8 @@ export default defineComponent({
return { return {
dataLoading: true, dataLoading: true,
vedirectConfigList: {} as VedirectConfig, vedirectConfigList: {} as VedirectConfig,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
}; };
}, },
@ -61,7 +73,7 @@ export default defineComponent({
methods: { methods: {
getVedirectConfig() { getVedirectConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/vedirect/config", { headers: authHeader() }) fetch('/api/vedirect/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.vedirectConfigList = data; this.vedirectConfigList = data;
@ -72,22 +84,20 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.vedirectConfigList)); formData.append('data', JSON.stringify(this.vedirectConfigList));
fetch("/api/vedirect/config", { fetch('/api/vedirect/config', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((response) => {
(response) => { this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
this.alertMessage = this.$t('apiresponse.' + response.code, response.param); this.alertType = response.type;
this.alertType = response.type; this.showAlert = true;
this.showAlert = true; });
}
);
}, },
}, },
}); });
</script> </script>

View File

@ -7,19 +7,31 @@
<tr> <tr>
<th>{{ $t('vedirectinfo.Status') }}</th> <th>{{ $t('vedirectinfo.Status') }}</th>
<td> <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> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('vedirectinfo.VerboseLogging') }}</th> <th>{{ $t('vedirectinfo.VerboseLogging') }}</th>
<td> <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> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('vedirectinfo.UpdatesOnly') }}</th> <th>{{ $t('vedirectinfo.UpdatesOnly') }}</th>
<td> <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> </td>
</tr> </tr>
</tbody> </tbody>
@ -33,7 +45,7 @@
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import StatusBadge from '@/components/StatusBadge.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 { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -41,7 +53,7 @@ export default defineComponent({
components: { components: {
BasePage, BasePage,
CardElement, CardElement,
StatusBadge StatusBadge,
}, },
data() { data() {
return { return {
@ -55,7 +67,7 @@ export default defineComponent({
methods: { methods: {
getVedirectInfo() { getVedirectInfo() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/vedirect/status", { headers: authHeader() }) fetch('/api/vedirect/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.vedirectDataList = data; this.vedirectDataList = data;