webapp: Apply auto formatter

This commit is contained in:
Thomas Basler 2024-07-05 21:57:53 +02:00
parent d8316db20f
commit 342642ec01
66 changed files with 2281 additions and 1419 deletions

View File

@ -5,10 +5,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import NavBar from "./components/NavBar.vue"; import NavBar from './components/NavBar.vue';
export default defineComponent({ export default defineComponent({
name: "App", name: 'App',
components: { components: {
NavBar, NavBar,
}, },

View File

@ -1,19 +1,29 @@
<template> <template>
<div :class="{'container-xxl': !isWideScreen, <div :class="{ 'container-xxl': !isWideScreen, 'container-fluid': isWideScreen }" role="main">
'container-fluid': isWideScreen}" role="main">
<div class="page-header"> <div class="page-header">
<div class="row"> <div class="row">
<div class="col-sm-11"> <div class="col-sm-11">
<h1>{{ title }} <h1>
<span v-if="showWebSocket" :class="{ {{ title }}
'onlineMarker': isWebsocketConnected, <span
'offlineMarker': !isWebsocketConnected, v-if="showWebSocket"
}"></span> :class="{
onlineMarker: isWebsocketConnected,
offlineMarker: !isWebsocketConnected,
}"
></span>
</h1> </h1>
</div> </div>
<div class="col-sm-1" v-if="showReload"> <div class="col-sm-1" v-if="showReload">
<button type="button" class="float-end btn btn-outline-primary" <button
@click="$emit('reload')" v-tooltip :title="$t('base.Reload')" ><BIconArrowClockwise /></button> type="button"
class="float-end btn btn-outline-primary"
@click="$emit('reload')"
v-tooltip
:title="$t('base.Reload')"
>
<BIconArrowClockwise />
</button>
</div> </div>
</div> </div>
</div> </div>
@ -48,7 +58,7 @@ export default defineComponent({
showReload: { type: Boolean, required: false, default: false }, showReload: { type: Boolean, required: false, default: false },
}, },
mounted() { mounted() {
console.log("init"); console.log('init');
PullToRefresh.init({ PullToRefresh.init({
mainElement: 'body', // above which element? mainElement: 'body', // above which element?
instructionsPullToRefresh: this.$t('base.Pull'), instructionsPullToRefresh: this.$t('base.Pull'),
@ -56,11 +66,11 @@ export default defineComponent({
instructionsRefreshing: this.$t('base.Refreshing'), instructionsRefreshing: this.$t('base.Refreshing'),
onRefresh: () => { onRefresh: () => {
this.$emit('reload'); this.$emit('reload');
} },
}); });
}, },
unmounted() { unmounted() {
console.log("destroy"); console.log('destroy');
PullToRefresh.destroyAll(); PullToRefresh.destroyAll();
}, },
}); });
@ -100,13 +110,15 @@ export default defineComponent({
margin: -12px 0 0 -12px; margin: -12px 0 0 -12px;
border: 1px solid #00bb00; border: 1px solid #00bb00;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 0 4px #00bb00, inset 0 0 4px rgb(56, 111, 169); box-shadow:
0 0 4px #00bb00,
inset 0 0 4px rgb(56, 111, 169);
transform: scale(0); transform: scale(0);
animation: online 2.5s ease-in-out infinite; animation: online 2.5s ease-in-out infinite;
} }
@keyframes online { @keyframes online {
0% { 0% {
transform: scale(.1); transform: scale(0.1);
opacity: 1; opacity: 1;
} }

View File

@ -1,44 +1,50 @@
<template> <template>
<div v-if="isAlertVisible" ref="element" class="alert" role="alert" :class="classes"> <div v-if="isAlertVisible" ref="element" class="alert" role="alert" :class="classes">
<slot /> <slot />
<button v-if="dismissible" type="button" class="btn-close" data-bs-dismiss="alert" :aria-label="dismissLabel" <button
@click="dismissClicked" /> v-if="dismissible"
type="button"
class="btn-close"
data-bs-dismiss="alert"
:aria-label="dismissLabel"
@click="dismissClicked"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Alert from "bootstrap/js/dist/alert"; import Alert from 'bootstrap/js/dist/alert';
import { computed, defineComponent, onBeforeUnmount, ref, watch } from "vue"; import { computed, defineComponent, onBeforeUnmount, ref, watch } from 'vue';
export const toInteger = (value: number, defaultValue = NaN) => { export const toInteger = (value: number, defaultValue = NaN) => {
return Number.isInteger(value) ? value : defaultValue; return Number.isInteger(value) ? value : defaultValue;
}; };
export default defineComponent({ export default defineComponent({
name: "BootstrapAlert", name: 'BootstrapAlert',
props: { props: {
dismissLabel: { type: String, default: "Close" }, dismissLabel: { type: String, default: 'Close' },
dismissible: { type: Boolean, default: false }, dismissible: { type: Boolean, default: false },
fade: { type: Boolean, default: false }, fade: { type: Boolean, default: false },
modelValue: { type: [Boolean, Number], default: false }, modelValue: { type: [Boolean, Number], default: false },
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
variant: { type: String, default: "info" }, variant: { type: String, default: 'info' },
}, },
emits: ["dismissed", "dismiss-count-down", "update:modelValue"], emits: ['dismissed', 'dismiss-count-down', 'update:modelValue'],
setup(props, { emit }) { setup(props, { emit }) {
const element = ref<HTMLElement>(); const element = ref<HTMLElement>();
const instance = ref<Alert>(); const instance = ref<Alert>();
const classes = computed(() => ({ const classes = computed(() => ({
[`alert-${props.variant}`]: props.variant, [`alert-${props.variant}`]: props.variant,
show: props.modelValue, show: props.modelValue,
"alert-dismissible": props.dismissible, 'alert-dismissible': props.dismissible,
fade: props.modelValue, fade: props.modelValue,
})); }));
let _countDownTimeout: number | undefined = 0; let _countDownTimeout: number | undefined = 0;
const parseCountDown = (value: boolean | number) => { const parseCountDown = (value: boolean | number) => {
if (typeof value === "boolean") { if (typeof value === 'boolean') {
return 0; return 0;
} }
@ -53,9 +59,12 @@ export default defineComponent({
}; };
const countDown = ref(); const countDown = ref();
watch(() => props.modelValue, () => { watch(
countDown.value = parseCountDown(props.modelValue); () => props.modelValue,
}); () => {
countDown.value = parseCountDown(props.modelValue);
}
);
const isAlertVisible = computed(() => props.modelValue || props.show); const isAlertVisible = computed(() => props.modelValue || props.show);
@ -85,12 +94,12 @@ export default defineComponent({
}; };
const dismissClicked = () => { const dismissClicked = () => {
if (typeof props.modelValue === "boolean") { if (typeof props.modelValue === 'boolean') {
emit("update:modelValue", false); emit('update:modelValue', false);
} else { } else {
emit("update:modelValue", 0); emit('update:modelValue', 0);
} }
emit("dismissed"); emit('dismissed');
}; };
watch(() => props.modelValue, handleShowAndModelChanged); watch(() => props.modelValue, handleShowAndModelChanged);
@ -98,10 +107,10 @@ export default defineComponent({
watch(countDown, (newValue) => { watch(countDown, (newValue) => {
clearCountDownInterval(); clearCountDownInterval();
if (typeof props.modelValue === "boolean") return; if (typeof props.modelValue === 'boolean') return;
emit("dismiss-count-down", newValue); emit('dismiss-count-down', newValue);
if (newValue === 0 && props.modelValue > 0) emit("dismissed"); if (newValue === 0 && props.modelValue > 0) emit('dismissed');
if (props.modelValue !== newValue) emit("update:modelValue", newValue); if (props.modelValue !== newValue) emit('update:modelValue', newValue);
if (newValue > 0) { if (newValue > 0) {
_countDownTimeout = setTimeout(() => { _countDownTimeout = setTimeout(() => {
countDown.value--; countDown.value--;

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="['card', addSpace ? 'mt-5' : '' ]"> <div :class="['card', addSpace ? 'mt-5' : '']">
<div :class="['card-header', textVariant]">{{ text }}</div> <div :class="['card-header', textVariant]">{{ text }}</div>
<div :class="['card-body', 'card-text', centerContent ? 'text-center' : '']"> <div :class="['card-body', 'card-text', centerContent ? 'text-center' : '']">
<slot /> <slot />
@ -12,10 +12,10 @@ import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
'text': String, text: String,
'textVariant': String, textVariant: String,
'addSpace': Boolean, addSpace: Boolean,
'centerContent': Boolean, centerContent: Boolean,
}, },
}); });
</script> </script>

View File

@ -1,8 +1,7 @@
<template> <template>
<BootstrapAlert :show="!devInfoList.valid_data"> <BootstrapAlert :show="!devInfoList.valid_data">
<h4 class="alert-heading"> <h4 class="alert-heading"><BIconInfoSquare class="fs-2" />&nbsp;{{ $t('devinfo.NoInfo') }}</h4>
<BIconInfoSquare class="fs-2" />&nbsp;{{ $t('devinfo.NoInfo') }} {{ $t('devinfo.NoInfoLong') }}
</h4>{{ $t('devinfo.NoInfoLong') }}
</BootstrapAlert> </BootstrapAlert>
<table v-if="devInfoList.valid_data" class="table table-hover"> <table v-if="devInfoList.valid_data" class="table table-hover">
<tbody> <tbody>
@ -53,7 +52,7 @@
<script lang="ts"> <script lang="ts">
import BootstrapAlert from '@/components/BootstrapAlert.vue'; import BootstrapAlert from '@/components/BootstrapAlert.vue';
import type { DevInfoStatus } from "@/types/DevInfoStatus"; import type { DevInfoStatus } from '@/types/DevInfoStatus';
import { BIconInfoSquare } from 'bootstrap-icons-vue'; import { BIconInfoSquare } from 'bootstrap-icons-vue';
import { defineComponent, type PropType } from 'vue'; import { defineComponent, type PropType } from 'vue';
@ -70,20 +69,20 @@ export default defineComponent({
return (value: number) => { return (value: number) => {
const version_major = Math.floor(value / 10000); const version_major = Math.floor(value / 10000);
const version_minor = Math.floor((value - version_major * 10000) / 100); const version_minor = Math.floor((value - version_major * 10000) / 100);
const version_patch = Math.floor((value - version_major * 10000 - version_minor * 100)); const version_patch = Math.floor(value - version_major * 10000 - version_minor * 100);
return version_major + "." + version_minor + "." + version_patch; return version_major + '.' + version_minor + '.' + version_patch;
}; };
}, },
productionYear() { productionYear() {
return() => { return () => {
return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xF) + 2014; return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xf) + 2014;
} };
}, },
productionWeek() { productionWeek() {
return() => { return () => {
return ((parseInt(this.devInfoList.serial, 16) >> (5 * 4)) & 0xFF).toString(16); return ((parseInt(this.devInfoList.serial, 16) >> (5 * 4)) & 0xff).toString(16);
} };
} },
} },
}); });
</script> </script>

View File

@ -36,4 +36,4 @@ export default defineComponent({
}, },
}, },
}); });
</script> </script>

View File

@ -17,10 +17,16 @@
</tr> </tr>
<tr> <tr>
<th>{{ $t('firmwareinfo.FirmwareVersion') }}</th> <th>{{ $t('firmwareinfo.FirmwareVersion') }}</th>
<td><a :href="versionInfoUrl" <td>
target="_blank" v-tooltip :title="$t('firmwareinfo.FirmwareVersionHint')"> <a
:href="versionInfoUrl"
target="_blank"
v-tooltip
:title="$t('firmwareinfo.FirmwareVersionHint')"
>
{{ systemStatus.git_hash }} {{ systemStatus.git_hash }}
</a></td> </a>
</td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('firmwareinfo.PioEnv') }}</th> <th>{{ $t('firmwareinfo.PioEnv') }}</th>
@ -30,16 +36,32 @@
<th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th> <th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th>
<td> <td>
<div class="form-check form-check-inline form-switch"> <div class="form-check form-check-inline form-switch">
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" /> <input
v-model="modelAllowVersionInfo"
class="form-check-input"
type="checkbox"
role="switch"
v-tooltip
:title="$t('firmwareinfo.FrmwareUpdateAllow')"
/>
<label class="form-check-label"> <label class="form-check-label">
<a v-if="modelAllowVersionInfo && systemStatus.update_url !== undefined" :href="systemStatus.update_url" target="_blank" v-tooltip <a
:title="$t('firmwareinfo.FirmwareUpdateHint')"> v-if="modelAllowVersionInfo && systemStatus.update_url !== undefined"
:href="systemStatus.update_url"
target="_blank"
v-tooltip
:title="$t('firmwareinfo.FirmwareUpdateHint')"
>
<span class="badge" :class="systemStatus.update_status"> <span class="badge" :class="systemStatus.update_status">
{{ systemStatus.update_text }} {{ systemStatus.update_text }}
</span> </span>
</a> </a>
<span v-else-if="modelAllowVersionInfo" class="badge" :class="systemStatus.update_status"> <span
{{ systemStatus.update_text }} v-else-if="modelAllowVersionInfo"
class="badge"
:class="systemStatus.update_status"
>
{{ systemStatus.update_text }}
</span> </span>
</label> </label>
</div> </div>
@ -59,7 +81,9 @@
</tr> </tr>
<tr> <tr>
<th>{{ $t('firmwareinfo.Uptime') }}</th> <th>{{ $t('firmwareinfo.Uptime') }}</th>
<td>{{ $t('firmwareinfo.UptimeValue', timeInHours(systemStatus.uptime)) }}</td> <td>
{{ $t('firmwareinfo.UptimeValue', timeInHours(systemStatus.uptime)) }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -93,7 +117,7 @@ export default defineComponent({
timeInHours() { timeInHours() {
return (value: number) => { return (value: number) => {
const [count, time] = timestampToString(this.$i18n.locale, value, true); const [count, time] = timestampToString(this.$i18n.locale, value, true);
return {count, time}; return { count, time };
}; };
}, },
versionInfoUrl(): string { versionInfoUrl(): string {
@ -101,7 +125,7 @@ export default defineComponent({
return 'https://github.com/tbnobody/OpenDTU/commits/' + this.systemStatus.git_hash; return 'https://github.com/tbnobody/OpenDTU/commits/' + this.systemStatus.git_hash;
} }
return 'https://github.com/tbnobody/OpenDTU/releases/tag/' + this.systemStatus.git_hash; return 'https://github.com/tbnobody/OpenDTU/releases/tag/' + this.systemStatus.git_hash;
} },
}, },
}); });
</script> </script>

View File

@ -1,7 +1,9 @@
<template> <template>
<hr class="border border-3 opacity-75"> <hr class="border border-3 opacity-75" />
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="button" class="btn btn-secondary" @click="$emit('reload')">{{ $t('base.Cancel') }}</button> <button type="button" class="btn btn-secondary" @click="$emit('reload')">
{{ $t('base.Cancel') }}
</button>
<button type="submit" class="btn btn-primary">{{ $t('base.Save') }}</button> <button type="submit" class="btn btn-primary">{{ $t('base.Save') }}</button>
</div> </div>
</template> </template>

View File

@ -3,8 +3,14 @@
<th>{{ name }}</th> <th>{{ name }}</th>
<td> <td>
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" :style="{ width: getPercent() + '%' }" <div
v-bind:aria-valuenow="getPercent()" aria-valuemin="0" aria-valuemax="100"> class="progress-bar"
role="progressbar"
:style="{ width: getPercent() + '%' }"
v-bind:aria-valuenow="getPercent()"
aria-valuemin="0"
aria-valuemax="100"
>
{{ $n(getPercent() / 100, 'percent') }} {{ $n(getPercent() / 100, 'percent') }}
</div> </div>
</div> </div>

View File

@ -1,47 +1,57 @@
<template> <template>
<BootstrapAlert :show="!hasValidData"> <BootstrapAlert :show="!hasValidData">
<h4 class="alert-heading"> <h4 class="alert-heading"><BIconInfoSquare class="fs-2" />&nbsp;{{ $t('gridprofile.NoInfo') }}</h4>
<BIconInfoSquare class="fs-2" />&nbsp;{{ $t('gridprofile.NoInfo') }} {{ $t('gridprofile.NoInfoLong') }}
</h4>{{ $t('gridprofile.NoInfoLong') }}
</BootstrapAlert> </BootstrapAlert>
<template v-if="hasValidData"> <template v-if="hasValidData">
<table class="table table-hover"> <table class="table table-hover">
<tbody> <tbody>
<tr> <tr>
<td>{{ $t('gridprofile.Name') }}</td> <td>{{ $t('gridprofile.Name') }}</td>
<td>{{ gridProfileList.name }}</td> <td>{{ gridProfileList.name }}</td>
</tr> </tr>
<tr> <tr>
<td>{{ $t('gridprofile.Version') }}</td> <td>{{ $t('gridprofile.Version') }}</td>
<td>{{ gridProfileList.version }}</td> <td>{{ gridProfileList.version }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="accordion" id="accordionProfile"> <div class="accordion" id="accordionProfile">
<div class="accordion-item" v-for="(section, index) in gridProfileList.sections" :key="index"> <div class="accordion-item" v-for="(section, index) in gridProfileList.sections" :key="index">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" :data-bs-target="`#collapse${index}`" aria-expanded="true" :aria-controls="`collapse${index}`"> <button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
:data-bs-target="`#collapse${index}`"
aria-expanded="true"
:aria-controls="`collapse${index}`"
>
{{ section.name }} {{ section.name }}
</button> </button>
</h2> </h2>
<div :id="`collapse${index}`" class="accordion-collapse collapse" data-bs-parent="#accordionProfile"> <div :id="`collapse${index}`" class="accordion-collapse collapse" data-bs-parent="#accordionProfile">
<div class="accordion-body"> <div class="accordion-body">
<table class="table table-hover"> <table class="table table-hover">
<tbody> <tbody>
<tr v-for="value in section.items" :key="value.n"> <tr v-for="value in section.items" :key="value.n">
<th>{{ value.n }}</th> <th>{{ value.n }}</th>
<td> <td>
<template v-if="value.u!='bool'"> <template v-if="value.u != 'bool'">
{{ $n(value.v, 'decimal') }} {{ value.u }} {{ $n(value.v, 'decimal') }} {{ value.u }}
</template> </template>
<template v-else> <template v-else>
<StatusBadge :status="value.v==1" true_text="gridprofile.Enabled" false_text="gridprofile.Disabled"/> <StatusBadge
</template> :status="value.v == 1"
</td> true_text="gridprofile.Enabled"
</tr> false_text="gridprofile.Disabled"
</tbody> />
</template>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@ -53,7 +63,14 @@
<div class="accordion" id="accordionDev"> <div class="accordion" id="accordionDev">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDev" aria-expanded="true" aria-controls="collapseDev"> <button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseDev"
aria-expanded="true"
aria-controls="collapseDev"
>
{{ $t('gridprofile.GridprofileSupport') }} {{ $t('gridprofile.GridprofileSupport') }}
</button> </button>
</h2> </h2>
@ -62,7 +79,8 @@
<BootstrapAlert :show="true" variant="danger"> <BootstrapAlert :show="true" variant="danger">
<h4 class="info-heading"> <h4 class="info-heading">
<BIconInfoSquare class="fs-2" />&nbsp;{{ $t('gridprofile.GridprofileSupport') }} <BIconInfoSquare class="fs-2" />&nbsp;{{ $t('gridprofile.GridprofileSupport') }}
</h4><div v-html="$t('gridprofile.GridprofileSupportLong')"></div> </h4>
<div v-html="$t('gridprofile.GridprofileSupportLong')"></div>
</BootstrapAlert> </BootstrapAlert>
<samp> <samp>
{{ rawContent() }} {{ rawContent() }}
@ -71,15 +89,13 @@
</div> </div>
</div> </div>
</div> </div>
</template> </template>
</template> </template>
<script lang="ts"> <script lang="ts">
import BootstrapAlert from '@/components/BootstrapAlert.vue'; import BootstrapAlert from '@/components/BootstrapAlert.vue';
import type { GridProfileRawdata } from '@/types/GridProfileRawdata'; import type { GridProfileRawdata } from '@/types/GridProfileRawdata';
import type { GridProfileStatus } from "@/types/GridProfileStatus"; import type { GridProfileStatus } from '@/types/GridProfileStatus';
import { BIconInfoSquare } from 'bootstrap-icons-vue'; import { BIconInfoSquare } from 'bootstrap-icons-vue';
import { defineComponent, type PropType } from 'vue'; import { defineComponent, type PropType } from 'vue';
import StatusBadge from './StatusBadge.vue'; import StatusBadge from './StatusBadge.vue';
@ -97,12 +113,14 @@ export default defineComponent({
computed: { computed: {
rawContent() { rawContent() {
return () => { return () => {
return this.gridProfileRawList.raw.map(function (x) { return this.gridProfileRawList.raw
let y = x.toString(16); // to hex .map(function (x) {
y = ("00" + y).substr(-2); // zero-pad to 2-digits let y = x.toString(16); // to hex
return y y = ('00' + y).substr(-2); // zero-pad to 2-digits
}).join(' '); return y;
} })
.join(' ');
};
}, },
hasValidData() { hasValidData() {
return this.gridProfileRawList.raw.reduce((sum, x) => sum + x, 0) > 0; return this.gridProfileRawList.raw.reduce((sum, x) => sum + x, 0) > 0;

View File

@ -9,7 +9,9 @@
</tr> </tr>
<tr> <tr>
<th>{{ $t('heapdetails.LargestFreeBlock') }}</th> <th>{{ $t('heapdetails.LargestFreeBlock') }}</th>
<td>{{ $n(Math.round(systemStatus.heap_max_block / 1024), 'kilobyte') }}</td> <td>
{{ $n(Math.round(systemStatus.heap_max_block / 1024), 'kilobyte') }}
</td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('heapdetails.Fragmentation') }}</th> <th>{{ $t('heapdetails.Fragmentation') }}</th>
@ -17,7 +19,11 @@
</tr> </tr>
<tr> <tr>
<th>{{ $t('heapdetails.MaxUsage') }}</th> <th>{{ $t('heapdetails.MaxUsage') }}</th>
<td>{{ $n(Math.round(getMaxUsageAbs() / 1024), 'kilobyte') }} ({{ $n(getMaxUsageRel(), 'percent') }})</td> <td>
{{ $n(Math.round(getMaxUsageAbs() / 1024), 'kilobyte') }} ({{
$n(getMaxUsageRel(), 'percent')
}})
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -48,7 +54,7 @@ export default defineComponent({
return this.getMaxUsageAbs() / this.systemStatus.heap_total; return this.getMaxUsageAbs() / this.systemStatus.heap_total;
}, },
getFragmentation() { getFragmentation() {
return 1 - (this.systemStatus.heap_max_block / this.getFreeHeap()); return 1 - this.systemStatus.heap_max_block / this.getFreeHeap();
}, },
}, },
}); });

View File

@ -17,11 +17,7 @@
<script lang="ts"> <script lang="ts">
import BootstrapAlert from '@/components/BootstrapAlert.vue'; import BootstrapAlert from '@/components/BootstrapAlert.vue';
import type { Hints } from '@/types/LiveDataStatus'; import type { Hints } from '@/types/LiveDataStatus';
import { import { BIconBroadcast, BIconClock, BIconExclamationCircle } from 'bootstrap-icons-vue';
BIconBroadcast,
BIconClock,
BIconExclamationCircle
} from 'bootstrap-icons-vue';
import { defineComponent, type PropType } from 'vue'; import { defineComponent, type PropType } from 'vue';
export default defineComponent({ export default defineComponent({
@ -36,11 +32,11 @@ export default defineComponent({
}, },
methods: { methods: {
gotoTimeSettings() { gotoTimeSettings() {
this.$router.push("/settings/ntp"); this.$router.push('/settings/ntp');
}, },
gotoPasswordSettings() { gotoPasswordSettings() {
this.$router.push("/settings/security"); this.$router.push('/settings/security');
} },
} },
}); });
</script> </script>

View File

@ -2,25 +2,19 @@
<div class="row mb-3"> <div class="row mb-3">
<label <label
:for="inputId" :for="inputId"
:class="[ wide ? 'col-sm-4' : 'col-sm-2', isCheckbox ? 'form-check-label' : 'col-form-label' ]" :class="[wide ? 'col-sm-4' : 'col-sm-2', isCheckbox ? 'form-check-label' : 'col-form-label']"
> >
{{ label }} {{ label }}
<BIconInfoCircle v-if="tooltip !== undefined" v-tooltip :title="tooltip" /> <BIconInfoCircle v-if="tooltip !== undefined" v-tooltip :title="tooltip" />
</label> </label>
<div :class="[ wide ? 'col-sm-8' : 'col-sm-10' ]"> <div :class="[wide ? 'col-sm-8' : 'col-sm-10']">
<div v-if="!isTextarea" <div v-if="!isTextarea" :class="{ 'form-check form-switch': isCheckbox, 'input-group': postfix || prefix }">
:class="{'form-check form-switch': isCheckbox, <span v-if="prefix" class="input-group-text" :id="descriptionId">
'input-group': postfix || prefix }"
>
<span v-if="prefix"
class="input-group-text"
:id="descriptionId"
>
{{ prefix }} {{ prefix }}
</span> </span>
<input <input
v-model="model" v-model="model"
:class="[ isCheckbox ? 'form-check-input' : 'form-control' ]" :class="[isCheckbox ? 'form-check-input' : 'form-control']"
:id="inputId" :id="inputId"
:placeholder="placeholder" :placeholder="placeholder"
:type="type" :type="type"
@ -31,13 +25,10 @@
:disabled="disabled" :disabled="disabled"
:aria-describedby="descriptionId" :aria-describedby="descriptionId"
/> />
<span v-if="postfix" <span v-if="postfix" class="input-group-text" :id="descriptionId">
class="input-group-text"
:id="descriptionId"
>
{{ postfix }} {{ postfix }}
</span> </span>
<slot/> <slot />
</div> </div>
<div v-else> <div v-else>
<textarea <textarea
@ -63,20 +54,20 @@ export default defineComponent({
BIconInfoCircle, BIconInfoCircle,
}, },
props: { props: {
'modelValue': [String, Number, Boolean, Date], modelValue: [String, Number, Boolean, Date],
'label': String, label: String,
'placeholder': String, placeholder: String,
'type': String, type: String,
'maxlength': String, maxlength: String,
'min': String, min: String,
'max': String, max: String,
'step': String, step: String,
'rows': String, rows: String,
'disabled': Boolean, disabled: Boolean,
'postfix': String, postfix: String,
'prefix': String, prefix: String,
'wide': Boolean, wide: Boolean,
'tooltip': String, tooltip: String,
}, },
data() { data() {
return {}; return {};
@ -97,11 +88,13 @@ export default defineComponent({
// normally, the label is sufficient to build a unique id // normally, the label is sufficient to build a unique id
// if two inputs with the same label text on one page is required, // if two inputs with the same label text on one page is required,
// use a unique placeholder even if it is a checkbox // use a unique placeholder even if it is a checkbox
return this.label?.replace(/[^A-Za-z0-9]/g, '') + return (
(this.placeholder ? this.placeholder.replace(/[^A-Za-z0-9]/g, '') : ''); this.label?.replace(/[^A-Za-z0-9]/g, '') +
(this.placeholder ? this.placeholder.replace(/[^A-Za-z0-9]/g, '') : '')
);
}, },
inputId() { inputId() {
return 'input' + this.uniqueLabel return 'input' + this.uniqueLabel;
}, },
descriptionId() { descriptionId() {
return 'desc' + this.uniqueLabel; return 'desc' + this.uniqueLabel;
@ -111,7 +104,7 @@ export default defineComponent({
}, },
isCheckbox() { isCheckbox() {
return this.type === 'checkbox'; return this.type === 'checkbox';
} },
}, },
}); });
</script> </script>

View File

@ -14,16 +14,16 @@ export default defineComponent({
BootstrapAlert, BootstrapAlert,
}, },
props: { props: {
'modelValue': { type: [String, Number], required: true }, modelValue: { type: [String, Number], required: true },
'id': String, id: String,
'inputClass': String, inputClass: String,
'required': Boolean, required: Boolean,
}, },
data() { data() {
return { return {
inputSerial: "", inputSerial: '',
formatHint: "", formatHint: '',
formatShow: "info", formatShow: 'info',
}; };
}, },
computed: { computed: {
@ -45,13 +45,13 @@ export default defineComponent({
inputSerial: function (val) { inputSerial: function (val) {
const serial = val.toString().toUpperCase(); // Convert to lowercase for case-insensitivity const serial = val.toString().toUpperCase(); // Convert to lowercase for case-insensitivity
if (serial == "") { if (serial == '') {
this.formatHint = ""; this.formatHint = '';
this.model = ""; this.model = '';
return; return;
} }
this.formatShow = "info"; this.formatShow = 'info';
// Contains only numbers // Contains only numbers
if (/^1{1}[\dA-F]{11}$/.test(serial)) { if (/^1{1}[\dA-F]{11}$/.test(serial)) {
@ -70,30 +70,31 @@ export default defineComponent({
if (this.checkHerfChecksum(serial)) { if (this.checkHerfChecksum(serial)) {
this.model = this.convertHerfToHoy(serial); this.model = this.convertHerfToHoy(serial);
this.$nextTick(() => { this.$nextTick(() => {
this.formatHint = this.$t('inputserial.format_herf_valid', { serial: this.model }); this.formatHint = this.$t('inputserial.format_herf_valid', {
serial: this.model,
});
}); });
} else { } else {
this.formatHint = this.$t('inputserial.format_herf_invalid'); this.formatHint = this.$t('inputserial.format_herf_invalid');
this.formatShow = "danger"; this.formatShow = 'danger';
} }
// Any other format // Any other format
} else { } else {
this.formatHint = this.$t('inputserial.format_unknown'); this.formatHint = this.$t('inputserial.format_unknown');
this.formatShow = "danger"; this.formatShow = 'danger';
} }
} },
}, },
methods: { methods: {
checkHerfChecksum(sn: string) { checkHerfChecksum(sn: string) {
const chars64 = 'HMFLGW5XC301234567899Z67YRT2S8ABCDEFGHJKDVEJ4KQPUALMNPRSTUVWXYNB'; const chars64 = 'HMFLGW5XC301234567899Z67YRT2S8ABCDEFGHJKDVEJ4KQPUALMNPRSTUVWXYNB';
const checksum = sn.substring(sn.indexOf("-") + 1); const checksum = sn.substring(sn.indexOf('-') + 1);
const serial = sn.substring(0, sn.indexOf("-")); const serial = sn.substring(0, sn.indexOf('-'));
const first_char = '1'; const first_char = '1';
const i = chars32.indexOf(first_char) const i = chars32.indexOf(first_char);
const sum1: number = Array.from(serial).reduce((sum, c) => sum + c.charCodeAt(0), 0) & 31; const sum1: number = Array.from(serial).reduce((sum, c) => sum + c.charCodeAt(0), 0) & 31;
const sum2: number = Array.from(serial).reduce((sum, c) => sum + chars32.indexOf(c), 0) & 31; const sum2: number = Array.from(serial).reduce((sum, c) => sum + chars32.indexOf(c), 0) & 31;
const ext = first_char + chars64[sum1 + i] + chars64[sum2 + i]; const ext = first_char + chars64[sum1 + i] + chars64[sum2 + i];
@ -106,11 +107,11 @@ export default defineComponent({
for (let i = 0; i < 9; i++) { for (let i = 0; i < 9; i++) {
const pos: bigint = BigInt(chars32.indexOf(sn[i].toUpperCase())); const pos: bigint = BigInt(chars32.indexOf(sn[i].toUpperCase()));
const shift: bigint = BigInt(42 - 5 * i - (i <= 2 ? 0 : 2)); const shift: bigint = BigInt(42 - 5 * i - (i <= 2 ? 0 : 2));
sn_int |= (pos << shift); sn_int |= pos << shift;
} }
return sn_int.toString(16); return sn_int.toString(16);
} },
}, },
}); });
</script> </script>

View File

@ -1,5 +1,6 @@
<template> <template>
<CardElement :text="$t('interfacenetworkinfo.NetworkInterface', { iface: networkStatus.network_mode })" <CardElement
:text="$t('interfacenetworkinfo.NetworkInterface', { iface: networkStatus.network_mode })"
textVariant="text-bg-primary" textVariant="text-bg-primary"
> >
<div class="table-responsive"> <div class="table-responsive">

View File

@ -1,8 +1,12 @@
<template> <template>
<div class="card" :class="{ <div
'border-info': channelType == 'AC', class="card"
'border-secondary': channelType == 'INV' :class="{
}" style="overflow: hidden"> 'border-info': channelType == 'AC',
'border-secondary': channelType == 'INV',
}"
style="overflow: hidden"
>
<div v-if="channelType == 'INV'" class="card-header text-bg-secondary"> <div v-if="channelType == 'INV'" class="card-header text-bg-secondary">
{{ $t('inverterchannelinfo.General') }} {{ $t('inverterchannelinfo.General') }}
</div> </div>
@ -22,11 +26,12 @@
<tr v-for="(property, key) in channelData" :key="`prop-${key}`"> <tr v-for="(property, key) in channelData" :key="`prop-${key}`">
<template v-if="key != 'name' && property"> <template v-if="key != 'name' && property">
<th scope="row">{{ $t('inverterchannelproperty.' + key) }}</th> <th scope="row">{{ $t('inverterchannelproperty.' + key) }}</th>
<td style="text-align: right; padding-right: 0;"> <td style="text-align: right; padding-right: 0">
{{ $n(property.v, 'decimal', { {{
minimumFractionDigits: property.d, $n(property.v, 'decimal', {
maximumFractionDigits: property.d minimumFractionDigits: property.d,
}) maximumFractionDigits: property.d,
})
}} }}
</td> </td>
<td>{{ property.u }}</td> <td>{{ property.u }}</td>

View File

@ -3,10 +3,12 @@
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalYieldTotal')"> <CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalYieldTotal')">
<h2> <h2>
{{ $n(totalData.YieldTotal.v, 'decimal', { {{
minimumFractionDigits: totalData.YieldTotal.d, $n(totalData.YieldTotal.v, 'decimal', {
maximumFractionDigits: totalData.YieldTotal.d minimumFractionDigits: totalData.YieldTotal.d,
}) }} maximumFractionDigits: totalData.YieldTotal.d,
})
}}
<small class="text-muted">{{ totalData.YieldTotal.u }}</small> <small class="text-muted">{{ totalData.YieldTotal.u }}</small>
</h2> </h2>
</CardElement> </CardElement>
@ -14,10 +16,12 @@
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalYieldDay')"> <CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalYieldDay')">
<h2> <h2>
{{ $n(totalData.YieldDay.v, 'decimal', { {{
minimumFractionDigits: totalData.YieldDay.d, $n(totalData.YieldDay.v, 'decimal', {
maximumFractionDigits: totalData.YieldDay.d minimumFractionDigits: totalData.YieldDay.d,
}) }} maximumFractionDigits: totalData.YieldDay.d,
})
}}
<small class="text-muted">{{ totalData.YieldDay.u }}</small> <small class="text-muted">{{ totalData.YieldDay.u }}</small>
</h2> </h2>
</CardElement> </CardElement>
@ -25,10 +29,12 @@
<div class="col"> <div class="col">
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalPower')"> <CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.TotalPower')">
<h2> <h2>
{{ $n(totalData.Power.v, 'decimal', { {{
minimumFractionDigits: totalData.Power.d, $n(totalData.Power.v, 'decimal', {
maximumFractionDigits: totalData.Power.d minimumFractionDigits: totalData.Power.d,
}) }} maximumFractionDigits: totalData.Power.d,
})
}}
<small class="text-muted">{{ totalData.Power.u }}</small> <small class="text-muted">{{ totalData.Power.u }}</small>
</h2> </h2>
</CardElement> </CardElement>

View File

@ -11,21 +11,21 @@ import { defineComponent } from 'vue';
import { LOCALES } from '@/locales'; import { LOCALES } from '@/locales';
export default defineComponent({ export default defineComponent({
name: "LocaleSwitcher", name: 'LocaleSwitcher',
methods: { methods: {
updateLanguage() { updateLanguage() {
localStorage.setItem("locale", this.$i18n.locale); localStorage.setItem('locale', this.$i18n.locale);
}, },
getLocaleName(locale: string): string { getLocaleName(locale: string): string {
return LOCALES.find(i => i.value === locale)?.caption || ""; return LOCALES.find((i) => i.value === locale)?.caption || '';
} },
}, },
mounted() { mounted() {
if (localStorage.getItem("locale")) { if (localStorage.getItem('locale')) {
this.$i18n.locale = localStorage.getItem("locale") || "en"; this.$i18n.locale = localStorage.getItem('locale') || 'en';
} else { } else {
localStorage.setItem("locale", this.$i18n.locale); localStorage.setItem('locale', this.$i18n.locale);
} }
}, },
}); });
</script> </script>

View File

@ -12,14 +12,26 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<FsInfo :name="$t('memoryinfo.Heap')" :total="systemStatus.heap_total" <FsInfo
:used="systemStatus.heap_used" /> :name="$t('memoryinfo.Heap')"
<FsInfo :name="$t('memoryinfo.PsRam')" :total="systemStatus.psram_total" :total="systemStatus.heap_total"
:used="systemStatus.psram_used" /> :used="systemStatus.heap_used"
<FsInfo :name="$t('memoryinfo.LittleFs')" :total="systemStatus.littlefs_total" />
:used="systemStatus.littlefs_used" /> <FsInfo
<FsInfo :name="$t('memoryinfo.Sketch')" :total="systemStatus.sketch_total" :name="$t('memoryinfo.PsRam')"
:used="systemStatus.sketch_used" /> :total="systemStatus.psram_total"
:used="systemStatus.psram_used"
/>
<FsInfo
:name="$t('memoryinfo.LittleFs')"
:total="systemStatus.littlefs_total"
:used="systemStatus.littlefs_used"
/>
<FsInfo
:name="$t('memoryinfo.Sketch')"
:total="systemStatus.sketch_total"
:used="systemStatus.sketch_used"
/>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -28,7 +40,7 @@
<script lang="ts"> <script lang="ts">
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import FsInfo from "@/components/FsInfo.vue"; import FsInfo from '@/components/FsInfo.vue';
import type { SystemStatus } from '@/types/SystemStatus'; import type { SystemStatus } from '@/types/SystemStatus';
import { defineComponent, type PropType } from 'vue'; import { defineComponent, type PropType } from 'vue';

View File

@ -4,8 +4,13 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">{{ title }}</h5> <h5 class="modal-title">{{ title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="getCloseText" <button
@click="close"></button> type="button"
class="btn-close"
data-bs-dismiss="modal"
:aria-label="getCloseText"
@click="close"
></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="text-center" v-if="loading"> <div class="text-center" v-if="loading">
@ -13,14 +18,13 @@
<span class="visually-hidden">{{ $t('home.Loading') }}</span> <span class="visually-hidden">{{ $t('home.Loading') }}</span>
</div> </div>
</div> </div>
<slot v-else> <slot v-else> </slot>
</slot>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<slot name="footer"> <slot name="footer"> </slot>
</slot> <button type="button" class="btn btn-secondary" @click="close" data-bs-dismiss="modal">
<button type="button" class="btn btn-secondary" @click="close" data-bs-dismiss="modal">{{ {{ getCloseText }}
getCloseText }}</button> </button>
</div> </div>
</div> </div>
</div> </div>
@ -32,16 +36,16 @@ import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
'modalId': { type: String, required: true }, modalId: { type: String, required: true },
'title': { type: String, required: true }, title: { type: String, required: true },
'closeText': { type: String, required: false, default: '' }, closeText: { type: String, required: false, default: '' },
'small': Boolean, small: Boolean,
'loading': Boolean, loading: Boolean,
}, },
computed: { computed: {
getCloseText() { getCloseText() {
return this.closeText == '' ? this.$t('base.Close') : this.closeText; return this.closeText == '' ? this.$t('base.Close') : this.closeText;
} },
}, },
methods: { methods: {
close() { close() {

View File

@ -1,19 +1,24 @@
<template> <template>
<nav class="navbar navbar-expand-md fixed-top bg-body-tertiary" data-bs-theme="dark"> <nav class="navbar navbar-expand-md fixed-top bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid"> <div class="container-fluid">
<router-link @click="onClick" class="navbar-brand" to="/" style="display: flex; height: 30px; padding: 0;"> <router-link @click="onClick" class="navbar-brand" to="/" style="display: flex; height: 30px; padding: 0">
<BIconTree v-if="isXmas" width="30" height="30" class="d-inline-block align-text-top text-success" /> <BIconTree v-if="isXmas" width="30" height="30" class="d-inline-block align-text-top text-success" />
<BIconEgg v-else-if="isEaster" width="30" height="30" class="d-inline-block align-text-top text-info" /> <BIconEgg v-else-if="isEaster" width="30" height="30" class="d-inline-block align-text-top text-info" />
<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: .5rem"> <span style="margin-left: 0.5rem"> OpenDTU </span>
OpenDTU
</span>
</router-link> </router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" <button
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation"> class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" ref="navbarCollapse" id="navbarNavAltMarkup"> <div class="collapse navbar-collapse" ref="navbarCollapse" id="navbarNavAltMarkup">
@ -22,71 +27,111 @@
<router-link @click="onClick" class="nav-link" to="/">{{ $t('menu.LiveView') }}</router-link> <router-link @click="onClick" class="nav-link" to="/">{{ $t('menu.LiveView') }}</router-link>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarScrollingDropdown" role="button" <a
data-bs-toggle="dropdown" aria-expanded="false"> class="nav-link dropdown-toggle"
href="#"
id="navbarScrollingDropdown"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ $t('menu.Settings') }} {{ $t('menu.Settings') }}
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarScrollingDropdown"> <ul class="dropdown-menu" aria-labelledby="navbarScrollingDropdown">
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/network">{{ $t('menu.NetworkSettings') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/network">{{
$t('menu.NetworkSettings')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/ntp">{{ $t('menu.NTPSettings') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/ntp">{{
$t('menu.NTPSettings')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/mqtt">{{ $t('menu.MQTTSettings') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/mqtt">{{
$t('menu.MQTTSettings')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/inverter">{{ $t('menu.InverterSettings') }} <router-link @click="onClick" class="dropdown-item" to="/settings/inverter"
>{{ $t('menu.InverterSettings') }}
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/security">{{ $t('menu.SecuritySettings') }} <router-link @click="onClick" class="dropdown-item" to="/settings/security"
>{{ $t('menu.SecuritySettings') }}
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/dtu">{{ $t('menu.DTUSettings') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/dtu">{{
$t('menu.DTUSettings')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/device">{{ $t('menu.DeviceManager') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/device">{{
$t('menu.DeviceManager')
}}</router-link>
</li> </li>
<li> <li>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/settings/config">{{ $t('menu.ConfigManagement') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/settings/config">{{
$t('menu.ConfigManagement')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/firmware/upgrade">{{ $t('menu.FirmwareUpgrade') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/firmware/upgrade">{{
$t('menu.FirmwareUpgrade')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/maintenance/reboot">{{ $t('menu.DeviceReboot') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/maintenance/reboot">{{
$t('menu.DeviceReboot')
}}</router-link>
</li> </li>
</ul> </ul>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarScrollingDropdown" role="button" <a
data-bs-toggle="dropdown" aria-expanded="false"> class="nav-link dropdown-toggle"
href="#"
id="navbarScrollingDropdown"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ $t('menu.Info') }} {{ $t('menu.Info') }}
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarScrollingDropdown"> <ul class="dropdown-menu" aria-labelledby="navbarScrollingDropdown">
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/info/system">{{ $t('menu.System') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/info/system">{{
$t('menu.System')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/info/network">{{ $t('menu.Network') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/info/network">{{
$t('menu.Network')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/info/ntp">{{ $t('menu.NTP') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/info/ntp">{{
$t('menu.NTP')
}}</router-link>
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/info/mqtt">{{ $t('menu.MQTT') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/info/mqtt">{{
$t('menu.MQTT')
}}</router-link>
</li> </li>
<li> <li>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
</li> </li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/info/console">{{ $t('menu.Console') }}</router-link> <router-link @click="onClick" class="dropdown-item" to="/info/console">{{
$t('menu.Console')
}}</router-link>
</li> </li>
</ul> </ul>
</li> </li>
@ -97,8 +142,12 @@
<ThemeSwitcher class="me-2" /> <ThemeSwitcher class="me-2" />
<form class="d-flex" role="search"> <form class="d-flex" role="search">
<LocaleSwitcher class="me-2" /> <LocaleSwitcher class="me-2" />
<button v-if="isLogged" class="btn btn-outline-danger" @click="signout">{{ $t('menu.Logout') }}</button> <button v-if="isLogged" class="btn btn-outline-danger" @click="signout">
<button v-if="!isLogged" class="btn btn-outline-success" @click="signin">{{ $t('menu.Login') }}</button> {{ $t('menu.Logout') }}
</button>
<button v-if="!isLogged" class="btn btn-outline-success" @click="signin">
{{ $t('menu.Login') }}
</button>
</form> </form>
</ul> </ul>
</div> </div>
@ -125,24 +174,24 @@ export default defineComponent({
return { return {
isLogged: this.isLoggedIn(), isLogged: this.isLoggedIn(),
now: {} as Date, now: {} as Date,
} };
}, },
created() { created() {
this.$emitter.on("logged-in", () => { this.$emitter.on('logged-in', () => {
this.isLogged = this.isLoggedIn(); this.isLogged = this.isLoggedIn();
}); });
this.$emitter.on("logged-out", () => { this.$emitter.on('logged-out', () => {
this.isLogged = this.isLoggedIn(); this.isLogged = this.isLoggedIn();
}); });
this.now = new Date(); this.now = new Date();
setInterval(() => { setInterval(() => {
this.now = new Date(); this.now = new Date();
}, 10000) }, 10000);
}, },
computed: { computed: {
isXmas() { isXmas() {
return (this.now.getMonth() + 1 == 12 && (this.now.getDate() >= 24 && this.now.getDate() <= 26)); return this.now.getMonth() + 1 == 12 && this.now.getDate() >= 24 && this.now.getDate() <= 26;
}, },
isEaster() { isEaster() {
const easter = this.getEasterSunday(this.now.getFullYear()); const easter = this.getEasterSunday(this.now.getFullYear());
@ -163,11 +212,11 @@ export default defineComponent({
signout(e: Event) { signout(e: Event) {
e.preventDefault(); e.preventDefault();
this.logout(); this.logout();
this.$emitter.emit("logged-out"); this.$emitter.emit('logged-out');
this.$router.push('/'); this.$router.push('/');
}, },
onClick() { onClick() {
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show"); this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove('show');
}, },
getEasterSunday(year: number): Date { getEasterSunday(year: number): Date {
const f = Math.floor; const f = Math.floor;
@ -181,7 +230,7 @@ export default defineComponent({
const day = L + 28 - 31 * f(month / 4); const day = L + 28 - 31 * f(month / 4);
return new Date(year, month - 1, day); return new Date(year, month - 1, day);
} },
}, },
}); });
</script> </script>

View File

@ -11,18 +11,23 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="(category) in categories" :key="category"> <template v-for="category in categories" :key="category">
<tr v-for="(prop, prop_idx) in properties(category)" :key="prop"> <tr v-for="(prop, prop_idx) in properties(category)" :key="prop">
<td v-if="prop_idx == 0" :rowspan="properties(category).length"> <td v-if="prop_idx == 0" :rowspan="properties(category).length">
{{ capitalizeFirstLetter(category) }}</td> {{ capitalizeFirstLetter(category) }}
<td :class="{ 'table-danger': !isEqual(category, prop) }">{{ prop }}</td> </td>
<td :class="{ 'table-danger': !isEqual(category, prop) }">
{{ prop }}
</td>
<td> <td>
<template v-if="selectedPinAssignment && category in selectedPinAssignment"> <template v-if="selectedPinAssignment && category in selectedPinAssignment">
{{ (selectedPinAssignment as any)[category][prop] }}</template> {{ (selectedPinAssignment as any)[category][prop] }}</template
>
</td> </td>
<td> <td>
<template v-if="currentPinAssignment && category in currentPinAssignment"> <template v-if="currentPinAssignment && category in currentPinAssignment">
{{ (currentPinAssignment as any)[category][prop] }}</template> {{ (currentPinAssignment as any)[category][prop] }}</template
>
</td> </td>
</tr> </tr>
</template> </template>
@ -59,7 +64,9 @@ export default defineComponent({
let total: Array<string> = []; let total: Array<string> = [];
total = total.concat(curArray, selArray); total = total.concat(curArray, selArray);
return Array.from(new Set(total)).filter(cat => cat != 'name' && cat != 'links').sort(); return Array.from(new Set(total))
.filter((cat) => cat != 'name' && cat != 'links')
.sort();
}, },
}, },
methods: { methods: {
@ -105,6 +112,6 @@ export default defineComponent({
capitalizeFirstLetter(value: string): string { capitalizeFirstLetter(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1); return value.charAt(0).toUpperCase() + value.slice(1);
}, },
} },
}); });
</script> </script>

View File

@ -4,58 +4,83 @@
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>
<tr> <tr>
<th>{{ $t('radioinfo.Status', { module: "nRF24" }) }}</th> <th>{{ $t('radioinfo.Status', { module: 'nRF24' }) }}</th>
<td> <td>
<StatusBadge :status="systemStatus.nrf_configured" true_text="radioinfo.Configured" false_text="radioinfo.NotConfigured" false_class="text-bg-secondary" /> <StatusBadge
:status="systemStatus.nrf_configured"
true_text="radioinfo.Configured"
false_text="radioinfo.NotConfigured"
false_class="text-bg-secondary"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('radioinfo.ChipStatus', { module: "nRF24" }) }}</th> <th>{{ $t('radioinfo.ChipStatus', { module: 'nRF24' }) }}</th>
<td> <td>
<span class="badge" :class="{ <span
'text-bg-danger': systemStatus.nrf_configured && !systemStatus.nrf_connected, class="badge"
'text-bg-success': systemStatus.nrf_configured && systemStatus.nrf_connected, :class="{
}"> 'text-bg-danger': systemStatus.nrf_configured && !systemStatus.nrf_connected,
<template 'text-bg-success': systemStatus.nrf_configured && systemStatus.nrf_connected,
v-if="systemStatus.nrf_configured && systemStatus.nrf_connected">{{ $t('radioinfo.Connected') }}</template> }"
<template >
v-else-if="systemStatus.nrf_configured && !systemStatus.nrf_connected">{{ $t('radioinfo.NotConnected') }}</template> <template v-if="systemStatus.nrf_configured && systemStatus.nrf_connected">{{
$t('radioinfo.Connected')
}}</template>
<template v-else-if="systemStatus.nrf_configured && !systemStatus.nrf_connected">{{
$t('radioinfo.NotConnected')
}}</template>
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('radioinfo.ChipType', { module: "nRF24" }) }}</th> <th>{{ $t('radioinfo.ChipType', { module: 'nRF24' }) }}</th>
<td> <td>
<span class="badge" :class="{ <span
'text-bg-danger': systemStatus.nrf_connected && !systemStatus.nrf_pvariant, class="badge"
'text-bg-success': systemStatus.nrf_connected && systemStatus.nrf_pvariant, :class="{
'text-bg-secondary': !systemStatus.nrf_connected, 'text-bg-danger': systemStatus.nrf_connected && !systemStatus.nrf_pvariant,
}"> 'text-bg-success': systemStatus.nrf_connected && systemStatus.nrf_pvariant,
<template 'text-bg-secondary': !systemStatus.nrf_connected,
v-if="systemStatus.nrf_connected && systemStatus.nrf_pvariant">nRF24L01+</template> }"
<template >
v-else-if="systemStatus.nrf_connected && !systemStatus.nrf_pvariant">nRF24L01</template> <template v-if="systemStatus.nrf_connected && systemStatus.nrf_pvariant"
>nRF24L01+</template
>
<template v-else-if="systemStatus.nrf_connected && !systemStatus.nrf_pvariant"
>nRF24L01</template
>
<template v-else>{{ $t('radioinfo.Unknown') }}</template> <template v-else>{{ $t('radioinfo.Unknown') }}</template>
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('radioinfo.Status', { module: "CMT2300A" }) }}</th> <th>{{ $t('radioinfo.Status', { module: 'CMT2300A' }) }}</th>
<td> <td>
<StatusBadge :status="systemStatus.cmt_configured" true_text="radioinfo.Configured" false_text="radioinfo.NotConfigured" false_class="text-bg-secondary" /> <StatusBadge
:status="systemStatus.cmt_configured"
true_text="radioinfo.Configured"
false_text="radioinfo.NotConfigured"
false_class="text-bg-secondary"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('radioinfo.ChipStatus', { module: "CMT2300A" }) }}</th> <th>{{ $t('radioinfo.ChipStatus', { module: 'CMT2300A' }) }}</th>
<td> <td>
<span class="badge" :class="{ <span
'text-bg-danger': systemStatus.cmt_configured && !systemStatus.cmt_connected, class="badge"
'text-bg-success': systemStatus.cmt_configured && systemStatus.cmt_connected, :class="{
}"> 'text-bg-danger': systemStatus.cmt_configured && !systemStatus.cmt_connected,
<template 'text-bg-success': systemStatus.cmt_configured && systemStatus.cmt_connected,
v-if="systemStatus.cmt_configured && systemStatus.cmt_connected">{{ $t('radioinfo.Connected') }}</template> }"
<template >
v-else-if="systemStatus.cmt_configured && !systemStatus.cmt_connected">{{ $t('radioinfo.NotConnected') }}</template> <template v-if="systemStatus.cmt_configured && systemStatus.cmt_connected">{{
$t('radioinfo.Connected')
}}</template>
<template v-else-if="systemStatus.cmt_configured && !systemStatus.cmt_connected">{{
$t('radioinfo.NotConnected')
}}</template>
</span> </span>
</td> </td>
</tr> </tr>

View File

@ -10,23 +10,23 @@ import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
'status': Boolean, status: Boolean,
'true_text': { true_text: {
type: String, type: String,
required: true required: true,
}, },
'false_text': { false_text: {
type: String, type: String,
required: true required: true,
}, },
'true_class': { true_class: {
type: String, type: String,
default: 'text-bg-success' default: 'text-bg-success',
}, },
'false_class': { false_class: {
type: String, type: String,
default: 'text-bg-danger' default: 'text-bg-danger',
} },
}, },
}); });
</script> </script>

View File

@ -1,28 +1,46 @@
<template> <template>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center" id="bd-theme" <button
type="button" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
aria-label="Toggle theme (auto)"> id="bd-theme"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
data-bs-display="static"
aria-label="Toggle theme (auto)"
>
<BIconCircleHalf class="bi my-1 theme-icon-active" /> <BIconCircleHalf class="bi my-1 theme-icon-active" />
</button> </button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li> <li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" <button
aria-pressed="false"> type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="light"
aria-pressed="false"
>
<BIconSunFill class="bi me-2 opacity-50 theme-icon" /> <BIconSunFill class="bi me-2 opacity-50 theme-icon" />
{{ $t('localeswitcher.Light') }} {{ $t('localeswitcher.Light') }}
</button> </button>
</li> </li>
<li> <li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" <button
aria-pressed="false"> type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="dark"
aria-pressed="false"
>
<BIconMoonStarsFill class="bi me-2 opacity-50 theme-icon" /> <BIconMoonStarsFill class="bi me-2 opacity-50 theme-icon" />
{{ $t('localeswitcher.Dark') }} {{ $t('localeswitcher.Dark') }}
</button> </button>
</li> </li>
<li> <li>
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" <button
aria-pressed="true"> type="button"
class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto"
aria-pressed="true"
>
<BIconCircleHalf class="bi me-2 opacity-50 theme-icon" /> <BIconCircleHalf class="bi me-2 opacity-50 theme-icon" />
{{ $t('localeswitcher.Auto') }} {{ $t('localeswitcher.Auto') }}
</button> </button>
@ -33,14 +51,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { import { BIconCircleHalf, BIconSunFill, BIconMoonStarsFill } from 'bootstrap-icons-vue';
BIconCircleHalf,
BIconSunFill,
BIconMoonStarsFill,
} from 'bootstrap-icons-vue';
export default defineComponent({ export default defineComponent({
name: "ThemeSwitcher", name: 'ThemeSwitcher',
components: { components: {
BIconCircleHalf, BIconCircleHalf,
BIconSunFill, BIconSunFill,
@ -70,9 +84,9 @@ export default defineComponent({
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`); const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
const svgOfActiveBtn = btnToActive?.querySelector('.theme-icon'); const svgOfActiveBtn = btnToActive?.querySelector('.theme-icon');
document.querySelectorAll('[data-bs-theme-value]').forEach(element => { document.querySelectorAll('[data-bs-theme-value]').forEach((element) => {
element.classList.remove('active'); element.classList.remove('active');
}) });
btnToActive?.classList.add('active'); btnToActive?.classList.add('active');
@ -92,15 +106,14 @@ export default defineComponent({
} }
}); });
document.querySelectorAll('[data-bs-theme-value]') document.querySelectorAll('[data-bs-theme-value]').forEach((toggle) => {
.forEach(toggle => { toggle.addEventListener('click', () => {
toggle.addEventListener('click', () => { const theme = toggle.getAttribute('data-bs-theme-value') || 'auto';
const theme = toggle.getAttribute('data-bs-theme-value') || 'auto'; localStorage.setItem('theme', theme);
localStorage.setItem('theme', theme); this.setTheme(theme);
this.setTheme(theme); this.showActiveTheme(theme);
this.showActiveTheme(theme);
})
}); });
});
}, },
}); });
</script> </script>

View File

@ -6,7 +6,11 @@
<tr> <tr>
<th>{{ $t('wifiapinfo.Status') }}</th> <th>{{ $t('wifiapinfo.Status') }}</th>
<td> <td>
<StatusBadge :status="networkStatus.ap_status" true_text="wifiapinfo.Enabled" false_text="wifiapinfo.Disabled" /> <StatusBadge
:status="networkStatus.ap_status"
true_text="wifiapinfo.Enabled"
false_text="wifiapinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -6,7 +6,11 @@
<tr> <tr>
<th>{{ $t('wifistationinfo.Status') }}</th> <th>{{ $t('wifistationinfo.Status') }}</th>
<td> <td>
<StatusBadge :status="networkStatus.sta_status" true_text="wifistationinfo.Enabled" false_text="wifistationinfo.Disabled" /> <StatusBadge
:status="networkStatus.sta_status"
true_text="wifistationinfo.Enabled"
false_text="wifistationinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -40,7 +44,7 @@ import { defineComponent, type PropType } from 'vue';
export default defineComponent({ export default defineComponent({
components: { components: {
CardElement, CardElement,
StatusBadge StatusBadge,
}, },
props: { props: {
networkStatus: { type: Object as PropType<NetworkStatus>, required: true }, networkStatus: { type: Object as PropType<NetworkStatus>, required: true },

View File

@ -4,4 +4,4 @@ declare module '@vue/runtime-core' {
} }
} }
export { } // Important! See note. export {}; // Important! See note.

View File

@ -1,4 +1,4 @@
import type { I18nOptions } from "vue-i18n"; import type { I18nOptions } from 'vue-i18n';
export enum Locales { export enum Locales {
EN = 'en', EN = 'en',
@ -10,22 +10,22 @@ export const LOCALES = [
{ value: Locales.EN, caption: 'English' }, { value: Locales.EN, caption: 'English' },
{ value: Locales.DE, caption: 'Deutsch' }, { value: Locales.DE, caption: 'Deutsch' },
{ value: Locales.FR, caption: 'Français' }, { value: Locales.FR, caption: 'Français' },
] ];
export const dateTimeFormats: I18nOptions["datetimeFormats"] = {}; export const dateTimeFormats: I18nOptions['datetimeFormats'] = {};
export const numberFormats: I18nOptions["numberFormats"] = {}; export const numberFormats: I18nOptions['numberFormats'] = {};
LOCALES.forEach((locale) => { LOCALES.forEach((locale) => {
dateTimeFormats[locale.value] = { dateTimeFormats[locale.value] = {
'datetime': { datetime: {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
second: 'numeric', second: 'numeric',
year: 'numeric', year: 'numeric',
month: 'numeric', month: 'numeric',
day: 'numeric', day: 'numeric',
hour12: false hour12: false,
} },
}; };
numberFormats[locale.value] = { numberFormats[locale.value] = {
@ -33,25 +33,34 @@ LOCALES.forEach((locale) => {
style: 'decimal', style: 'decimal',
}, },
decimalNoDigits: { decimalNoDigits: {
style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0 style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}, },
decimalTwoDigits: { decimalTwoDigits: {
style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}, },
percent: { percent: {
style: 'percent', style: 'percent',
}, },
byte: { byte: {
style: 'unit', unit: 'byte', style: 'unit',
unit: 'byte',
}, },
kilobyte: { kilobyte: {
style: 'unit', unit: 'kilobyte', style: 'unit',
unit: 'kilobyte',
}, },
megabyte: { megabyte: {
style: 'unit', unit: 'megabyte', style: 'unit',
unit: 'megabyte',
}, },
celsius: { celsius: {
style: 'unit', unit: 'celsius', maximumFractionDigits: 1, style: 'unit',
unit: 'celsius',
maximumFractionDigits: 1,
}, },
}; };
}); });

View File

@ -1,21 +1,21 @@
import messages from '@intlify/unplugin-vue-i18n/messages' import messages from '@intlify/unplugin-vue-i18n/messages';
import mitt from 'mitt' import mitt from 'mitt';
import { createApp } from 'vue' import { createApp } from 'vue';
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n';
import App from './App.vue' import App from './App.vue';
import { dateTimeFormats, defaultLocale, numberFormats } from './locales' import { dateTimeFormats, defaultLocale, numberFormats } from './locales';
import { tooltip } from './plugins/bootstrap' import { tooltip } from './plugins/bootstrap';
import router from './router' import router from './router';
import "bootstrap" import 'bootstrap';
import './scss/styles.scss' import './scss/styles.scss';
const app = createApp(App) const app = createApp(App);
const emitter = mitt(); const emitter = mitt();
app.config.globalProperties.$emitter = emitter; app.config.globalProperties.$emitter = emitter;
app.directive('tooltip', tooltip) app.directive('tooltip', tooltip);
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
@ -24,10 +24,10 @@ const i18n = createI18n({
fallbackLocale: defaultLocale, fallbackLocale: defaultLocale,
messages, messages,
datetimeFormats: dateTimeFormats, datetimeFormats: dateTimeFormats,
numberFormats: numberFormats numberFormats: numberFormats,
}) });
app.use(router) app.use(router);
app.use(i18n) app.use(i18n);
app.mount('#app') app.mount('#app');

View File

@ -3,5 +3,5 @@ import { Tooltip } from 'bootstrap';
export const tooltip = { export const tooltip = {
mounted(el: HTMLElement) { mounted(el: HTMLElement) {
new Tooltip(el); new Tooltip(el);
} },
} };

View File

@ -1,7 +1,7 @@
import AboutView from '@/views/AboutView.vue'; import AboutView from '@/views/AboutView.vue';
import ConfigAdminView from '@/views/ConfigAdminView.vue'; import ConfigAdminView from '@/views/ConfigAdminView.vue';
import ConsoleInfoView from '@/views/ConsoleInfoView.vue'; import ConsoleInfoView from '@/views/ConsoleInfoView.vue';
import DeviceAdminView from '@/views/DeviceAdminView.vue' import DeviceAdminView from '@/views/DeviceAdminView.vue';
import DtuAdminView from '@/views/DtuAdminView.vue'; 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';
@ -21,104 +21,104 @@ import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
linkActiveClass: "active", linkActiveClass: 'active',
routes: [ routes: [
{ {
path: '/', path: '/',
name: 'Home', name: 'Home',
component: HomeView component: HomeView,
}, },
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
component: LoginView component: LoginView,
}, },
{ {
path: '/error?status=:status&message=:message', path: '/error?status=:status&message=:message',
name: 'Error', name: 'Error',
component: ErrorView component: ErrorView,
}, },
{ {
path: '/about', path: '/about',
name: 'About', name: 'About',
component: AboutView component: AboutView,
}, },
{ {
path: '/info/network', path: '/info/network',
name: 'Network', name: 'Network',
component: NetworkInfoView component: NetworkInfoView,
}, },
{ {
path: '/info/system', path: '/info/system',
name: 'System', name: 'System',
component: SystemInfoView component: SystemInfoView,
}, },
{ {
path: '/info/ntp', path: '/info/ntp',
name: 'NTP', name: 'NTP',
component: NtpInfoView component: NtpInfoView,
}, },
{ {
path: '/info/mqtt', path: '/info/mqtt',
name: 'MqTT', name: 'MqTT',
component: MqttInfoView component: MqttInfoView,
}, },
{ {
path: '/info/console', path: '/info/console',
name: 'Web Console', name: 'Web Console',
component: ConsoleInfoView component: ConsoleInfoView,
}, },
{ {
path: '/settings/network', path: '/settings/network',
name: 'Network Settings', name: 'Network Settings',
component: NetworkAdminView component: NetworkAdminView,
}, },
{ {
path: '/settings/ntp', path: '/settings/ntp',
name: 'NTP Settings', name: 'NTP Settings',
component: NtpAdminView component: NtpAdminView,
}, },
{ {
path: '/settings/mqtt', path: '/settings/mqtt',
name: 'MqTT Settings', name: 'MqTT Settings',
component: MqttAdminView component: MqttAdminView,
}, },
{ {
path: '/settings/inverter', path: '/settings/inverter',
name: 'Inverter Settings', name: 'Inverter Settings',
component: InverterAdminView component: InverterAdminView,
}, },
{ {
path: '/settings/dtu', path: '/settings/dtu',
name: 'DTU Settings', name: 'DTU Settings',
component: DtuAdminView component: DtuAdminView,
}, },
{ {
path: '/settings/device', path: '/settings/device',
name: 'Device Manager', name: 'Device Manager',
component: DeviceAdminView component: DeviceAdminView,
}, },
{ {
path: '/firmware/upgrade', path: '/firmware/upgrade',
name: 'Firmware Upgrade', name: 'Firmware Upgrade',
component: FirmwareUpgradeView component: FirmwareUpgradeView,
}, },
{ {
path: '/settings/config', path: '/settings/config',
name: 'Config Management', name: 'Config Management',
component: ConfigAdminView component: ConfigAdminView,
}, },
{ {
path: '/settings/security', path: '/settings/security',
name: 'Security', name: 'Security',
component: SecurityAdminView component: SecurityAdminView,
}, },
{ {
path: '/maintenance/reboot', path: '/maintenance/reboot',
name: 'Device Reboot', name: 'Device Reboot',
component: MaintenanceRebootView component: MaintenanceRebootView,
} },
] ],
}); });
export default router; export default router;

View File

@ -1,6 +1,6 @@
// Import all of Bootstrap's CSS // Import all of Bootstrap's CSS
@import "~bootstrap/scss/bootstrap"; @import '~bootstrap/scss/bootstrap';
.container-fluid .row { .container-fluid .row {
font-feature-settings: "tnum"; font-feature-settings: 'tnum';
} }

View File

@ -4,4 +4,4 @@ export interface ConfigFileInfo {
export interface ConfigFileList { export interface ConfigFileList {
configs: Array<ConfigFileInfo>; configs: Array<ConfigFileInfo>;
} }

View File

@ -1,4 +1,4 @@
import type { Device } from "./PinMapping"; import type { Device } from './PinMapping';
export interface Display { export interface Display {
rotation: number; rotation: number;

View File

@ -8,4 +8,4 @@ export interface EventlogItem {
export interface EventlogItems { export interface EventlogItems {
count: number; count: number;
events: Array<EventlogItem>; events: Array<EventlogItem>;
} }

View File

@ -1,3 +1,3 @@
export interface GridProfileRawdata { export interface GridProfileRawdata {
raw: Array<number>; raw: Array<number>;
} }

View File

@ -13,4 +13,4 @@ export interface GridProfileStatus {
name: string; name: string;
version: string; version: string;
sections: Array<GridProfileSection>; sections: Array<GridProfileSection>;
} }

View File

@ -2,4 +2,4 @@ export interface LimitStatus {
limit_relative: number; limit_relative: number;
max_power: number; max_power: number;
limit_set_status: string; limit_set_status: string;
} }

View File

@ -6,11 +6,11 @@ export interface ValueObject {
} }
export interface InverterStatistics { export interface InverterStatistics {
name: ValueObject, name: ValueObject;
Power?: ValueObject; Power?: ValueObject;
Voltage?: ValueObject; Voltage?: ValueObject;
Current?: ValueObject; Current?: ValueObject;
"Power DC"?: ValueObject; 'Power DC'?: ValueObject;
YieldDay?: ValueObject; YieldDay?: ValueObject;
YieldTotal?: ValueObject; YieldTotal?: ValueObject;
Frequency?: ValueObject; Frequency?: ValueObject;

View File

@ -20,4 +20,4 @@ export interface NetworkStatus {
// InterfaceApInfo // InterfaceApInfo
ap_ip: string; ap_ip: string;
ap_mac: string; ap_mac: string;
} }

View File

@ -5,4 +5,4 @@ export interface NtpConfig {
latitude: number; latitude: number;
longitude: number; longitude: number;
sunsettype: number; sunsettype: number;
} }

View File

@ -1,11 +1,11 @@
export interface NtpStatus { export interface NtpStatus {
ntp_server: string; ntp_server: string;
ntp_timezone: string; ntp_timezone: string;
ntp_timezone_descr: string ntp_timezone_descr: string;
ntp_status: boolean; ntp_status: boolean;
ntp_localtime: string; ntp_localtime: string;
sun_risetime: string; sun_risetime: string;
sun_settime: string; sun_settime: string;
sun_isDayPeriod: boolean; sun_isDayPeriod: boolean;
sun_isSunsetAvailable: boolean; sun_isSunsetAvailable: boolean;
} }

View File

@ -14,7 +14,7 @@ export interface Cmt2300 {
sdio: number; sdio: number;
gpio2: number; gpio2: number;
gpio3: number; gpio3: number;
} }
export interface Ethernet { export interface Ethernet {
enabled: boolean; enabled: boolean;
@ -39,7 +39,7 @@ export interface Links {
url: string; url: string;
} }
export interface Device { export interface Device {
name: string; name: string;
links: Array<Links>; links: Array<Links>;
nrf24: Nrf24; nrf24: Nrf24;
@ -48,4 +48,4 @@ export interface Device {
display: Display; display: Display;
} }
export interface PinMapping extends Array<Device>{} export interface PinMapping extends Array<Device> {}

View File

@ -1,4 +1,4 @@
export interface SecurityConfig { export interface SecurityConfig {
password: string; password: string;
allow_readonly: boolean; allow_readonly: boolean;
} }

View File

@ -1,11 +1,11 @@
import type { Emitter, EventType } from "mitt"; import type { Emitter, EventType } from 'mitt';
import type { Router } from "vue-router"; import type { Router } from 'vue-router';
export function authHeader(): Headers { export function authHeader(): Headers {
// return authorization header with basic auth credentials // return authorization header with basic auth credentials
let user = null; let user = null;
try { try {
user = JSON.parse(localStorage.getItem('user') || ""); user = JSON.parse(localStorage.getItem('user') || '');
} catch { } catch {
// continue regardless of error // continue regardless of error
} }
@ -21,15 +21,15 @@ export function authHeader(): Headers {
export function authUrl(): string { export function authUrl(): string {
let user = null; let user = null;
try { try {
user = JSON.parse(localStorage.getItem('user') || ""); user = JSON.parse(localStorage.getItem('user') || '');
} catch { } catch {
// continue regardless of error // continue regardless of error
} }
if (user && user.authdata) { if (user && user.authdata) {
return encodeURIComponent(atob(user.authdata)).replace("%3A", ":") + '@'; return encodeURIComponent(atob(user.authdata)).replace('%3A', ':') + '@';
} }
return ""; return '';
} }
export function logout() { export function logout() {
@ -38,7 +38,7 @@ export function logout() {
} }
export function isLoggedIn(): boolean { export function isLoggedIn(): boolean {
return (localStorage.getItem('user') != null); return localStorage.getItem('user') != null;
} }
export function login(username: string, password: string) { export function login(username: string, password: string) {
@ -46,13 +46,13 @@ export function login(username: string, password: string) {
method: 'GET', method: 'GET',
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
'Authorization': 'Basic ' + btoa(unescape(encodeURIComponent(username + ':' + password))), Authorization: 'Basic ' + btoa(unescape(encodeURIComponent(username + ':' + password))),
}, },
}; };
return fetch('/api/security/authenticate', requestOptions) return fetch('/api/security/authenticate', requestOptions)
.then(handleAuthResponse) .then(handleAuthResponse)
.then(retVal => { .then((retVal) => {
// login successful if there's a user in the response // login successful if there's a user in the response
if (retVal) { if (retVal) {
// store user details and basic auth credentials in local storage // store user details and basic auth credentials in local storage
@ -65,21 +65,32 @@ export function login(username: string, password: string) {
}); });
} }
export function handleResponse(response: Response, emitter: Emitter<Record<EventType, unknown>>, router: Router, ignore_error: boolean = false) { export function handleResponse(
return response.text().then(text => { response: Response,
emitter: Emitter<Record<EventType, unknown>>,
router: Router,
ignore_error: boolean = false
) {
return response.text().then((text) => {
const data = text && JSON.parse(text); const data = text && JSON.parse(text);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) { if (response.status === 401) {
// auto logout if 401 response returned from api // auto logout if 401 response returned from api
logout(); logout();
emitter.emit("logged-out"); emitter.emit('logged-out');
router.push({ path: "/login", query: { returnUrl: router.currentRoute.value.fullPath } }); router.push({
path: '/login',
query: { returnUrl: router.currentRoute.value.fullPath },
});
return Promise.reject(); return Promise.reject();
} }
const error = { message: (data && data.message) || response.statusText, status: response.status || 0 }; const error = {
message: (data && data.message) || response.statusText,
status: response.status || 0,
};
if (!ignore_error) { if (!ignore_error) {
router.push({ name: "Error", params: error }); router.push({ name: 'Error', params: error });
} }
return Promise.reject(error); return Promise.reject(error);
} }
@ -89,7 +100,7 @@ export function handleResponse(response: Response, emitter: Emitter<Record<Event
} }
function handleAuthResponse(response: Response) { function handleAuthResponse(response: Response) {
return response.text().then(text => { return response.text().then((text) => {
const data = text && JSON.parse(text); const data = text && JSON.parse(text);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) { if (response.status === 401) {
@ -97,7 +108,7 @@ function handleAuthResponse(response: Response) {
logout(); logout();
} }
const error = "Invalid credentials"; const error = 'Invalid credentials';
return Promise.reject(error); return Promise.reject(error);
} }

View File

@ -1,16 +1,11 @@
import { isLoggedIn, login, logout } from './authentication'; import { isLoggedIn, login, logout } from './authentication';
import { timestampToString } from './time'; import { timestampToString } from './time';
export { export { timestampToString, login, logout, isLoggedIn };
timestampToString,
login,
logout,
isLoggedIn,
};
export default { export default {
timestampToString, timestampToString,
login, login,
logout, logout,
isLoggedIn, isLoggedIn,
} };

View File

@ -1,10 +1,17 @@
export function timestampToString(locale: string, timestampSeconds: number, includeDays: true): [number, string]; export function timestampToString(locale: string, timestampSeconds: number, includeDays: true): [number, string];
export function timestampToString(locale: string, timestampSeconds: number, includeDays?: false): [string]; export function timestampToString(locale: string, timestampSeconds: number, includeDays?: false): [string];
export function timestampToString(locale: string, timestampSeconds: number, includeDays = false): [number, string] | [string] { export function timestampToString(
const timeString = new Date(timestampSeconds * 1000).toLocaleTimeString(locale, { timeZone: "UTC", hour12: false }); locale: string,
timestampSeconds: number,
includeDays = false
): [number, string] | [string] {
const timeString = new Date(timestampSeconds * 1000).toLocaleTimeString(locale, {
timeZone: 'UTC',
hour12: false,
});
if (!includeDays) return [timeString]; if (!includeDays) return [timeString];
const secondsPerDay = 60 * 60 * 24; const secondsPerDay = 60 * 60 * 24;
const days = Math.floor(timestampSeconds / secondsPerDay); const days = Math.floor(timestampSeconds / secondsPerDay);
return [days, timeString]; return [days, timeString];
} }

View File

@ -3,15 +3,25 @@
<div class="accordion" id="accordionExample"> <div class="accordion" id="accordionExample">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingDocumentation"> <h2 class="accordion-header" id="headingDocumentation">
<button class="accordion-button" type="button" data-bs-toggle="collapse" <button
data-bs-target="#collapseDocumentation" aria-expanded="true" aria-controls="collapseOne"> class="accordion-button"
<span class="badge text-bg-secondary"> type="button"
<BIconInfoCircle class="fs-4" /> data-bs-toggle="collapse"
</span>&nbsp;{{ $t('about.Documentation') }} data-bs-target="#collapseDocumentation"
aria-expanded="true"
aria-controls="collapseOne"
>
<span class="badge text-bg-secondary"> <BIconInfoCircle class="fs-4" /> </span>&nbsp;{{
$t('about.Documentation')
}}
</button> </button>
</h2> </h2>
<div id="collapseDocumentation" class="accordion-collapse collapse show" aria-labelledby="headingDocumentation" <div
data-bs-parent="#accordionExample"> id="collapseDocumentation"
class="accordion-collapse collapse show"
aria-labelledby="headingDocumentation"
data-bs-parent="#accordionExample"
>
<div class="accordion-body"> <div class="accordion-body">
<p class="fw-normal" v-html="$t('about.DocumentationBody')"></p> <p class="fw-normal" v-html="$t('about.DocumentationBody')"></p>
</div> </div>
@ -19,15 +29,25 @@
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingOne"> <h2 class="accordion-header" id="headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <button
data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne"> class="accordion-button collapsed"
<span class="badge text-bg-secondary"> type="button"
<BIconInfoCircle class="fs-4" /> data-bs-toggle="collapse"
</span>&nbsp;{{ $t('about.ProjectOrigin') }} data-bs-target="#collapseOne"
aria-expanded="false"
aria-controls="collapseOne"
>
<span class="badge text-bg-secondary"> <BIconInfoCircle class="fs-4" /> </span>&nbsp;{{
$t('about.ProjectOrigin')
}}
</button> </button>
</h2> </h2>
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne" <div
data-bs-parent="#accordionExample"> id="collapseOne"
class="accordion-collapse collapse"
aria-labelledby="headingOne"
data-bs-parent="#accordionExample"
>
<div class="accordion-body"> <div class="accordion-body">
<p class="fw-normal" v-html="$t('about.ProjectOriginBody1')"></p> <p class="fw-normal" v-html="$t('about.ProjectOriginBody1')"></p>
<p class="fw-normal" v-html="$t('about.ProjectOriginBody2')"></p> <p class="fw-normal" v-html="$t('about.ProjectOriginBody2')"></p>
@ -38,61 +58,83 @@
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingTwo"> <h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <button
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> class="accordion-button collapsed"
<span class="badge text-bg-secondary"> type="button"
<BIconActivity class="fs-4" /> data-bs-toggle="collapse"
</span>&nbsp;{{ $t('about.NewsUpdates') }} data-bs-target="#collapseTwo"
aria-expanded="false"
aria-controls="collapseTwo"
>
<span class="badge text-bg-secondary"> <BIconActivity class="fs-4" /> </span>&nbsp;{{
$t('about.NewsUpdates')
}}
</button> </button>
</h2> </h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" <div
data-bs-parent="#accordionExample"> id="collapseTwo"
class="accordion-collapse collapse"
aria-labelledby="headingTwo"
data-bs-parent="#accordionExample"
>
<div class="accordion-body" v-html="$t('about.NewsUpdatesBody')"></div> <div class="accordion-body" v-html="$t('about.NewsUpdatesBody')"></div>
</div> </div>
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingThree"> <h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <button
data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree"> class="accordion-button collapsed"
<span class="badge text-bg-secondary"> type="button"
<BIconBug class="fs-4" /> data-bs-toggle="collapse"
</span>&nbsp;{{ $t('about.ErrorReporting') }} data-bs-target="#collapseThree"
aria-expanded="false"
aria-controls="collapseThree"
>
<span class="badge text-bg-secondary"> <BIconBug class="fs-4" /> </span>&nbsp;{{
$t('about.ErrorReporting')
}}
</button> </button>
</h2> </h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" <div
data-bs-parent="#accordionExample"> id="collapseThree"
class="accordion-collapse collapse"
aria-labelledby="headingThree"
data-bs-parent="#accordionExample"
>
<div class="accordion-body" v-html="$t('about.ErrorReportingBody')"></div> <div class="accordion-body" v-html="$t('about.ErrorReportingBody')"></div>
</div> </div>
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingFour"> <h2 class="accordion-header" id="headingFour">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" <button
data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour"> class="accordion-button collapsed"
<span class="badge text-bg-secondary"> type="button"
<BIconChat class="fs-4" /> data-bs-toggle="collapse"
</span>&nbsp;{{ $t('about.Discussion') }} data-bs-target="#collapseFour"
aria-expanded="false"
aria-controls="collapseFour"
>
<span class="badge text-bg-secondary"> <BIconChat class="fs-4" /> </span>&nbsp;{{
$t('about.Discussion')
}}
</button> </button>
</h2> </h2>
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" <div
data-bs-parent="#accordionExample"> id="collapseFour"
class="accordion-collapse collapse"
aria-labelledby="headingFour"
data-bs-parent="#accordionExample"
>
<div class="accordion-body" v-html="$t('about.DiscussionBody')"></div> <div class="accordion-body" v-html="$t('about.DiscussionBody')"></div>
</div> </div>
</div> </div>
</div> </div>
</BasePage> </BasePage>
</template> </template>
<script lang="ts"> <script lang="ts">
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import { import { BIconActivity, BIconBug, BIconChat, BIconInfoCircle } from 'bootstrap-icons-vue';
BIconActivity,
BIconBug,
BIconChat,
BIconInfoCircle
} from 'bootstrap-icons-vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
@ -104,5 +146,4 @@ export default defineComponent({
BIconInfoCircle, BIconInfoCircle,
}, },
}); });
</script> </script>

View File

@ -11,13 +11,14 @@
</div> </div>
<div class="col-sm"> <div class="col-sm">
<select class="form-select" v-model="backupFileSelect"> <select class="form-select" v-model="backupFileSelect">
<option v-for="(file) in fileList.configs" :key="file.name" :value="file.name"> <option v-for="file in fileList.configs" :key="file.name" :value="file.name">
{{ file.name }} {{ file.name }}
</option> </option>
</select> </select>
</div> </div>
<div class="col-sm"> <div class="col-sm">
<button class="btn btn-primary" @click="downloadConfig">{{ $t('configadmin.Backup') }} <button class="btn btn-primary" @click="downloadConfig">
{{ $t('configadmin.Backup') }}
</button> </button>
</div> </div>
</div> </div>
@ -33,9 +34,7 @@
</span> </span>
<br /> <br />
<br /> <br />
<button class="btn btn-light" @click="clear"> <button class="btn btn-light" @click="clear"><BIconArrowLeft /> {{ $t('configadmin.Back') }}</button>
<BIconArrowLeft /> {{ $t('configadmin.Back') }}
</button>
</div> </div>
<div v-else-if="!uploading && UploadSuccess"> <div v-else-if="!uploading && UploadSuccess">
@ -45,9 +44,7 @@
<span> {{ $t('configadmin.UploadSuccess') }} </span> <span> {{ $t('configadmin.UploadSuccess') }} </span>
<br /> <br />
<br /> <br />
<button class="btn btn-primary" @click="clear"> <button class="btn btn-primary" @click="clear"><BIconArrowLeft /> {{ $t('configadmin.Back') }}</button>
<BIconArrowLeft /> {{ $t('configadmin.Back') }}
</button>
</div> </div>
<div v-else-if="!uploading"> <div v-else-if="!uploading">
@ -62,7 +59,8 @@
<input class="form-control" type="file" ref="file" accept=".json" /> <input class="form-control" type="file" ref="file" accept=".json" />
</div> </div>
<div class="col-sm"> <div class="col-sm">
<button class="btn btn-primary" @click="uploadConfig">{{ $t('configadmin.Restore') }} <button class="btn btn-primary" @click="uploadConfig">
{{ $t('configadmin.Restore') }}
</button> </button>
</div> </div>
</div> </div>
@ -70,8 +68,14 @@
<div v-else-if="uploading"> <div v-else-if="uploading">
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" :style="{ width: progress + '%' }" <div
v-bind:aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100"> class="progress-bar"
role="progressbar"
:style="{ width: progress + '%' }"
v-bind:aria-valuenow="progress"
aria-valuemin="0"
aria-valuemax="100"
>
{{ progress }}% {{ progress }}%
</div> </div>
</div> </div>
@ -81,14 +85,20 @@
</CardElement> </CardElement>
<CardElement :text="$t('configadmin.ResetHeader')" textVariant="text-bg-primary" center-content add-space> <CardElement :text="$t('configadmin.ResetHeader')" textVariant="text-bg-primary" center-content add-space>
<button class="btn btn-danger" @click="onFactoryResetModal">{{ $t('configadmin.FactoryResetButton') }} <button class="btn btn-danger" @click="onFactoryResetModal">
{{ $t('configadmin.FactoryResetButton') }}
</button> </button>
<div class="alert alert-danger mt-3" role="alert" v-html="$t('configadmin.ResetHint')"></div> <div class="alert alert-danger mt-3" role="alert" v-html="$t('configadmin.ResetHint')"></div>
</CardElement> </CardElement>
</BasePage> </BasePage>
<ModalDialog modalId="factoryReset" small :title="$t('configadmin.FactoryReset')" :closeText="$t('configadmin.Cancel')"> <ModalDialog
modalId="factoryReset"
small
:title="$t('configadmin.FactoryReset')"
:closeText="$t('configadmin.Cancel')"
>
{{ $t('configadmin.ResetMsg') }} {{ $t('configadmin.ResetMsg') }}
<template #footer> <template #footer>
<button type="button" class="btn btn-danger" @click="onFactoryResetPerform"> <button type="button" class="btn btn-danger" @click="onFactoryResetPerform">
@ -100,17 +110,13 @@
<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 ModalDialog from '@/components/ModalDialog.vue'; import ModalDialog from '@/components/ModalDialog.vue';
import type { ConfigFileList } from '@/types/Config'; import type { ConfigFileList } from '@/types/Config';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
import { import { BIconArrowLeft, BIconCheckCircle, BIconExclamationCircleFill } from 'bootstrap-icons-vue';
BIconArrowLeft,
BIconCheckCircle,
BIconExclamationCircleFill
} from 'bootstrap-icons-vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
@ -126,18 +132,18 @@ export default defineComponent({
data() { data() {
return { return {
modalFactoryReset: {} as bootstrap.Modal, modalFactoryReset: {} as bootstrap.Modal,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
loading: true, loading: true,
uploading: false, uploading: false,
progress: 0, progress: 0,
UploadError: "", UploadError: '',
UploadSuccess: false, UploadSuccess: false,
file: {} as Blob, file: {} as Blob,
fileList: {} as ConfigFileList, fileList: {} as ConfigFileList,
backupFileSelect: "", backupFileSelect: '',
restoreFileSelect: "config.json", restoreFileSelect: 'config.json',
}; };
}, },
mounted() { mounted() {
@ -155,26 +161,24 @@ export default defineComponent({
}, },
onFactoryResetPerform() { onFactoryResetPerform() {
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify({ delete: true })); formData.append('data', JSON.stringify({ delete: true }));
fetch("/api/config/delete", { fetch('/api/config/delete', {
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; });
}
)
this.modalFactoryReset.hide(); this.modalFactoryReset.hide();
}, },
getFileList() { getFileList() {
this.loading = true; this.loading = true;
fetch("/api/config/list", { headers: authHeader() }) fetch('/api/config/list', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.fileList = data; this.fileList = data;
@ -185,9 +189,9 @@ export default defineComponent({
}); });
}, },
downloadConfig() { downloadConfig() {
fetch("/api/config/get?file=" + this.backupFileSelect, { headers: authHeader() }) fetch('/api/config/get?file=' + this.backupFileSelect, { headers: authHeader() })
.then(res => res.blob()) .then((res) => res.blob())
.then(blob => { .then((blob) => {
const file = window.URL.createObjectURL(blob); const file = window.URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = file; a.href = file;
@ -204,13 +208,13 @@ export default defineComponent({
if (target.files !== null && target.files?.length > 0) { if (target.files !== null && target.files?.length > 0) {
this.file = target.files[0]; this.file = target.files[0];
} else { } else {
this.UploadError = this.$t("configadmin.NoFileSelected"); this.UploadError = this.$t('configadmin.NoFileSelected');
this.uploading = false; this.uploading = false;
this.progress = 0; this.progress = 0;
return; return;
} }
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
request.addEventListener("load", () => { request.addEventListener('load', () => {
// request.response will hold the response from the server // request.response will hold the response from the server
if (request.status === 200) { if (request.status === 200) {
this.UploadSuccess = true; this.UploadSuccess = true;
@ -223,20 +227,20 @@ export default defineComponent({
this.progress = 0; this.progress = 0;
}); });
// Upload progress // Upload progress
request.upload.addEventListener("progress", (e) => { request.upload.addEventListener('progress', (e) => {
this.progress = Math.trunc((e.loaded / e.total) * 100); this.progress = Math.trunc((e.loaded / e.total) * 100);
}); });
request.withCredentials = true; request.withCredentials = true;
formData.append("config", this.file, "config"); formData.append('config', this.file, 'config');
request.open("post", "/api/config/upload?file=" + this.restoreFileSelect); request.open('post', '/api/config/upload?file=' + this.restoreFileSelect);
authHeader().forEach((value, key) => { authHeader().forEach((value, key) => {
request.setRequestHeader(key, value); request.setRequestHeader(key, value);
}); });
request.send(formData); request.send(formData);
}, },
clear() { clear() {
this.UploadError = ""; this.UploadError = '';
this.UploadSuccess = false; this.UploadSuccess = false;
this.getFileList(); this.getFileList();
}, },

View File

@ -4,8 +4,13 @@
<div class="row g-3 align-items-center"> <div class="row g-3 align-items-center">
<div class="col"> <div class="col">
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoScroll" <input
v-model="isAutoScroll"> class="form-check-input"
type="checkbox"
role="switch"
id="autoScroll"
v-model="isAutoScroll"
/>
<label class="form-check-label" for="autoScroll"> <label class="form-check-label" for="autoScroll">
{{ $t('console.EnableAutoScroll') }} {{ $t('console.EnableAutoScroll') }}
</label> </label>
@ -14,9 +19,11 @@
<div class="col text-end"> <div class="col text-end">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button type="button" class="btn btn-primary" :onClick="clearConsole"> <button type="button" class="btn btn-primary" :onClick="clearConsole">
{{ $t('console.ClearConsole') }}</button> {{ $t('console.ClearConsole') }}
</button>
<button type="button" class="btn btn-secondary" :onClick="copyConsole"> <button type="button" class="btn btn-secondary" :onClick="copyConsole">
{{ $t('console.CopyToClipboard') }}</button> {{ $t('console.CopyToClipboard') }}
</button>
</div> </div>
</div> </div>
</div> </div>
@ -41,7 +48,7 @@ export default defineComponent({
socket: {} as WebSocket, socket: {} as WebSocket,
heartInterval: 0, heartInterval: 0,
dataLoading: true, dataLoading: true,
consoleBuffer: "", consoleBuffer: '',
isAutoScroll: true, isAutoScroll: true,
endWithNewline: false, endWithNewline: false,
}; };
@ -56,21 +63,20 @@ export default defineComponent({
watch: { watch: {
consoleBuffer() { consoleBuffer() {
if (this.isAutoScroll) { if (this.isAutoScroll) {
const textarea = this.$el.querySelector("#console"); const textarea = this.$el.querySelector('#console');
setTimeout(() => { setTimeout(() => {
textarea.scrollTop = textarea.scrollHeight; textarea.scrollTop = textarea.scrollHeight;
}, 0); }, 0);
} }
} },
}, },
methods: { methods: {
initSocket() { initSocket() {
console.log("Starting connection to WebSocket Server"); console.log('Starting connection to 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}/console`;
}://${authString}${host}/console`;
this.closeSocket(); this.closeSocket();
this.socket = new WebSocket(webSocketUrl); this.socket = new WebSocket(webSocketUrl);
@ -84,14 +90,15 @@ export default defineComponent({
outstr = outstr.substring(0, outstr.length - 1); outstr = outstr.substring(0, outstr.length - 1);
removedNewline = true; removedNewline = true;
} }
this.consoleBuffer += (this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll("\n", "\n" + this.getOutDate()); this.consoleBuffer +=
(this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll('\n', '\n' + this.getOutDate());
this.endWithNewline = removedNewline; this.endWithNewline = removedNewline;
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 echo websocket server..."); console.log('Successfully connected to the echo 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
@ -105,7 +112,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
} }
@ -123,13 +130,19 @@ export default defineComponent({
}, },
getOutDate(): string { getOutDate(): string {
const u = new Date(); const u = new Date();
return ('0' + u.getHours()).slice(-2) + ':' + return (
('0' + u.getMinutes()).slice(-2) + ':' + ('0' + u.getHours()).slice(-2) +
('0' + u.getSeconds()).slice(-2) + '.' + ':' +
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' > '; ('0' + u.getMinutes()).slice(-2) +
':' +
('0' + u.getSeconds()).slice(-2) +
'.' +
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) +
' > '
);
}, },
clearConsole() { clearConsole() {
this.consoleBuffer = ""; this.consoleBuffer = '';
}, },
copyConsole() { copyConsole() {
const input = document.createElement('textarea'); const input = document.createElement('textarea');
@ -138,17 +151,17 @@ export default defineComponent({
input.select(); input.select();
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(input); document.body.removeChild(input);
} },
} },
}); });
</script> </script>
<style> <style>
#console { #console {
background-color: #0C0C0C; background-color: #0c0c0c;
color: #CCCCCC; color: #cccccc;
padding: 8px; padding: 8px;
font-family: courier new; font-family: courier new;
font-size: .875em; font-size: 0.875em;
} }
</style> </style>

View File

@ -7,19 +7,50 @@
<form @submit="savePinConfig"> <form @submit="savePinConfig">
<nav> <nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist"> <div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-pin-tab" data-bs-toggle="tab" data-bs-target="#nav-pin" <button
type="button" role="tab" aria-controls="nav-pin" aria-selected="true">{{ class="nav-link active"
$t('deviceadmin.PinAssignment') id="nav-pin-tab"
}}</button> data-bs-toggle="tab"
<button class="nav-link" id="nav-display-tab" data-bs-toggle="tab" data-bs-target="#nav-display" data-bs-target="#nav-pin"
type="button" role="tab" aria-controls="nav-display">{{ $t('deviceadmin.Display') }}</button> type="button"
<button class="nav-link" id="nav-leds-tab" data-bs-toggle="tab" data-bs-target="#nav-leds" role="tab"
type="button" role="tab" aria-controls="nav-leds">{{ $t('deviceadmin.Leds') }}</button> aria-controls="nav-pin"
aria-selected="true"
>
{{ $t('deviceadmin.PinAssignment') }}
</button>
<button
class="nav-link"
id="nav-display-tab"
data-bs-toggle="tab"
data-bs-target="#nav-display"
type="button"
role="tab"
aria-controls="nav-display"
>
{{ $t('deviceadmin.Display') }}
</button>
<button
class="nav-link"
id="nav-leds-tab"
data-bs-toggle="tab"
data-bs-target="#nav-leds"
type="button"
role="tab"
aria-controls="nav-leds"
>
{{ $t('deviceadmin.Leds') }}
</button>
</div> </div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-pin" role="tabpanel" aria-labelledby="nav-pin-tab" <div
tabindex="0"> class="tab-pane fade show active"
id="nav-pin"
role="tabpanel"
aria-labelledby="nav-pin-tab"
tabindex="0"
>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="row mb-3"> <div class="row mb-3">
@ -27,10 +58,21 @@
$t('deviceadmin.SelectedProfile') $t('deviceadmin.SelectedProfile')
}}</label> }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-select" id="inputPinProfile" <select
v-model="deviceConfigList.curPin.name"> class="form-select"
<option v-for="device in pinMappingList" :value="device.name" :key="device.name"> id="inputPinProfile"
{{ device.name === "Default" ? $t('deviceadmin.DefaultProfile') : device.name }} v-model="deviceConfigList.curPin.name"
>
<option
v-for="device in pinMappingList"
:value="device.name"
:key="device.name"
>
{{
device.name === 'Default'
? $t('deviceadmin.DefaultProfile')
: device.name
}}
</option> </option>
</select> </select>
</div> </div>
@ -39,33 +81,56 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col-sm-2"></div> <div class="col-sm-2"></div>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="btn-group mb-2 me-2" v-for="(doc, index) in pinMappingList.find(i => i.name === deviceConfigList.curPin.name)?.links" :key="index"> <div
class="btn-group mb-2 me-2"
v-for="(doc, index) in pinMappingList.find(
(i) => i.name === deviceConfigList.curPin.name
)?.links"
:key="index"
>
<a :href="doc.url" class="btn btn-primary" target="_blank">{{ doc.name }}</a> <a :href="doc.url" class="btn btn-primary" target="_blank">{{ doc.name }}</a>
</div> </div>
</div> </div>
</div> </div>
<div class="alert alert-danger mt-3" role="alert" v-html="$t('deviceadmin.ProfileHint')"> <div
</div> class="alert alert-danger mt-3"
role="alert"
v-html="$t('deviceadmin.ProfileHint')"
></div>
<PinInfo <PinInfo
:selectedPinAssignment="pinMappingList.find(i => i.name === deviceConfigList.curPin.name)" :selectedPinAssignment="
:currentPinAssignment="deviceConfigList.curPin" /> pinMappingList.find((i) => i.name === deviceConfigList.curPin.name)
"
:currentPinAssignment="deviceConfigList.curPin"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade show" id="nav-display" role="tabpanel" aria-labelledby="nav-display-tab" <div
tabindex="0"> class="tab-pane fade show"
id="nav-display"
role="tabpanel"
aria-labelledby="nav-display-tab"
tabindex="0"
>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<InputElement :label="$t('deviceadmin.PowerSafe')" <InputElement
v-model="deviceConfigList.display.power_safe" type="checkbox" :label="$t('deviceadmin.PowerSafe')"
:tooltip="$t('deviceadmin.PowerSafeHint')" /> v-model="deviceConfigList.display.power_safe"
type="checkbox"
:tooltip="$t('deviceadmin.PowerSafeHint')"
/>
<InputElement :label="$t('deviceadmin.Screensaver')" <InputElement
v-model="deviceConfigList.display.screensaver" type="checkbox" :label="$t('deviceadmin.Screensaver')"
:tooltip="$t('deviceadmin.ScreensaverHint')" /> v-model="deviceConfigList.display.screensaver"
type="checkbox"
:tooltip="$t('deviceadmin.ScreensaverHint')"
/>
<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">
@ -80,10 +145,15 @@
</div> </div>
</div> </div>
<InputElement :label="$t('deviceadmin.DiagramDuration')" <InputElement
v-model="deviceConfigList.display.diagramduration" type="number" :label="$t('deviceadmin.DiagramDuration')"
min=600 max=86400 v-model="deviceConfigList.display.diagramduration"
:tooltip="$t('deviceadmin.DiagramDurationHint')" :postfix="$t('deviceadmin.Seconds')" /> type="number"
min="600"
max="86400"
:tooltip="$t('deviceadmin.DiagramDurationHint')"
:postfix="$t('deviceadmin.Seconds')"
/>
<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">
@ -91,7 +161,11 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-select" v-model="deviceConfigList.display.language"> <select class="form-select" v-model="deviceConfigList.display.language">
<option v-for="language in displayLanguageList" :key="language.key" :value="language.key"> <option
v-for="language in displayLanguageList"
:key="language.key"
:value="language.key"
>
{{ $t(`deviceadmin.` + language.value) }} {{ $t(`deviceadmin.` + language.value) }}
</option> </option>
</select> </select>
@ -104,7 +178,11 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-select" v-model="deviceConfigList.display.rotation"> <select class="form-select" v-model="deviceConfigList.display.rotation">
<option v-for="rotation in displayRotationList" :key="rotation.key" :value="rotation.key"> <option
v-for="rotation in displayRotationList"
:key="rotation.key"
:value="rotation.key"
>
{{ $t(`deviceadmin.` + rotation.value) }} {{ $t(`deviceadmin.` + rotation.value) }}
</option> </option>
</select> </select>
@ -113,36 +191,57 @@
<div class="row mb-3"> <div class="row mb-3">
<label for="inputDisplayContrast" class="col-sm-2 col-form-label">{{ <label for="inputDisplayContrast" class="col-sm-2 col-form-label">{{
$t('deviceadmin.Contrast', { contrast: $n(deviceConfigList.display.contrast / 100, $t('deviceadmin.Contrast', {
'percent') contrast: $n(deviceConfigList.display.contrast / 100, 'percent'),
}) }}</label> })
}}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="range" class="form-range" min="0" max="100" id="inputDisplayContrast" <input
v-model="deviceConfigList.display.contrast" /> type="range"
class="form-range"
min="0"
max="100"
id="inputDisplayContrast"
v-model="deviceConfigList.display.contrast"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade show" id="nav-leds" role="tabpanel" aria-labelledby="nav-leds-tab" tabindex="0"> <div
class="tab-pane fade show"
id="nav-leds"
role="tabpanel"
aria-labelledby="nav-leds-tab"
tabindex="0"
>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<InputElement :label="$t('deviceadmin.EqualBrightness')" <InputElement
v-model="equalBrightnessCheckVal" type="checkbox" /> :label="$t('deviceadmin.EqualBrightness')"
v-model="equalBrightnessCheckVal"
type="checkbox"
/>
<div class="row mb-3" v-for="(ledSetting, index) in deviceConfigList.led" :key="index"> <div class="row mb-3" v-for="(ledSetting, index) in deviceConfigList.led" :key="index">
<label :for="getLedIdFromNumber(index)" class="col-sm-2 col-form-label">{{ <label :for="getLedIdFromNumber(index)" class="col-sm-2 col-form-label">{{
$t('deviceadmin.LedBrightness', { $t('deviceadmin.LedBrightness', {
led: index, led: index,
brightness: $n(ledSetting.brightness / 100, brightness: $n(ledSetting.brightness / 100, 'percent'),
'percent')
}) })
}}</label> }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="range" class="form-range" min="0" max="100" :id="getLedIdFromNumber(index)" <input
v-model="ledSetting.brightness" @change="syncSliders" /> type="range"
class="form-range"
min="0"
max="100"
:id="getLedIdFromNumber(index)"
v-model="ledSetting.brightness"
@change="syncSliders"
/>
</div> </div>
</div> </div>
</div> </div>
@ -150,20 +249,19 @@
</div> </div>
</div> </div>
<FormFooter @reload="getDeviceConfig"/> <FormFooter @reload="getDeviceConfig" />
</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 FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import PinInfo from '@/components/PinInfo.vue'; import PinInfo from '@/components/PinInfo.vue';
import type { DeviceConfig, Led } from "@/types/DeviceConfig"; import type { DeviceConfig, Led } from '@/types/DeviceConfig';
import type { PinMapping, Device } from "@/types/PinMapping"; import type { PinMapping, Device } from '@/types/PinMapping';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -181,8 +279,8 @@ export default defineComponent({
pinMappingLoading: true, pinMappingLoading: true,
deviceConfigList: {} as DeviceConfig, deviceConfigList: {} as DeviceConfig,
pinMappingList: {} as PinMapping, pinMappingList: {} as PinMapping,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
equalBrightnessCheckVal: false, equalBrightnessCheckVal: false,
displayRotationList: [ displayRotationList: [
@ -192,66 +290,64 @@ export default defineComponent({
{ key: 3, value: 'rot270' }, { key: 3, value: 'rot270' },
], ],
displayLanguageList: [ displayLanguageList: [
{ key: 0, value: "en" }, { key: 0, value: 'en' },
{ key: 1, value: "de" }, { key: 1, value: 'de' },
{ key: 2, value: "fr" }, { key: 2, value: 'fr' },
], ],
diagramModeList: [ diagramModeList: [
{ key: 0, value: "off" }, { key: 0, value: 'off' },
{ key: 1, value: "small" }, { key: 1, value: 'small' },
{ key: 2, value: "fullscreen" }, { key: 2, value: 'fullscreen' },
] ],
} };
}, },
created() { created() {
this.getDeviceConfig(); this.getDeviceConfig();
this.getPinMappingList(); this.getPinMappingList();
}, },
watch: { watch: {
equalBrightnessCheckVal: function(val) { equalBrightnessCheckVal: function (val) {
if (!val) { if (!val) {
return; return;
} }
this.deviceConfigList.led.every(v => v.brightness = this.deviceConfigList.led[0].brightness); this.deviceConfigList.led.every((v) => (v.brightness = this.deviceConfigList.led[0].brightness));
} },
}, },
methods: { methods: {
getPinMappingList() { getPinMappingList() {
this.pinMappingLoading = true; this.pinMappingLoading = true;
fetch("/api/config/get?file=pin_mapping.json", { headers: authHeader() }) fetch('/api/config/get?file=pin_mapping.json', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router, true)) .then((response) => handleResponse(response, this.$emitter, this.$router, true))
.then( .then((data) => {
(data) => { this.pinMappingList = data;
this.pinMappingList = data; })
}
)
.catch((error) => { .catch((error) => {
if (error.status != 404) { if (error.status != 404) {
this.alertMessage = this.$t('deviceadmin.ParseError', { error: error.message }); this.alertMessage = this.$t('deviceadmin.ParseError', {
error: error.message,
});
this.alertType = 'danger'; this.alertType = 'danger';
this.showAlert = true; this.showAlert = true;
} }
this.pinMappingList = Array<Device>(); this.pinMappingList = Array<Device>();
}) })
.finally(() => { .finally(() => {
this.pinMappingList.sort((a, b) => (a.name < b.name) ? -1 : 1); this.pinMappingList.sort((a, b) => (a.name < b.name ? -1 : 1));
this.pinMappingList.splice(0, 0, { "name": "Default" } as Device); this.pinMappingList.splice(0, 0, { name: 'Default' } as Device);
this.pinMappingLoading = false; this.pinMappingLoading = false;
}); });
}, },
getDeviceConfig() { getDeviceConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/device/config", { headers: authHeader() }) fetch('/api/device/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((data) => {
(data) => { this.deviceConfigList = data;
this.deviceConfigList = data; if (this.deviceConfigList.curPin.name === '') {
if (this.deviceConfigList.curPin.name === "") { this.deviceConfigList.curPin.name = 'Default';
this.deviceConfigList.curPin.name = "Default";
}
this.dataLoading = false;
} }
) this.dataLoading = false;
})
.then(() => { .then(() => {
this.equalBrightnessCheckVal = this.isEqualBrightness(); this.equalBrightnessCheckVal = this.isEqualBrightness();
}); });
@ -260,30 +356,28 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.deviceConfigList)); formData.append('data', JSON.stringify(this.deviceConfigList));
fetch("/api/device/config", { fetch('/api/device/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; });
}
);
}, },
getLedIdFromNumber(ledNo: number) : string { getLedIdFromNumber(ledNo: number): string {
return 'inputLED' + ledNo + 'Brightness'; return 'inputLED' + ledNo + 'Brightness';
}, },
getNumberFromLedId(id: string): number { getNumberFromLedId(id: string): number {
return parseInt(id.replace("inputLED", "").replace("Brightness", "")); return parseInt(id.replace('inputLED', '').replace('Brightness', ''));
}, },
isEqualBrightness(): boolean { isEqualBrightness(): boolean {
const allEqual = (arr : Led[]) => arr.every(v => v.brightness === arr[0].brightness); const allEqual = (arr: Led[]) => arr.every((v) => v.brightness === arr[0].brightness);
return allEqual(this.deviceConfigList.led); return allEqual(this.deviceConfigList.led);
}, },
syncSliders(event: Event) { syncSliders(event: Event) {
@ -291,8 +385,8 @@ export default defineComponent({
return; return;
} }
const srcId = this.getNumberFromLedId((event.target as Element).id); const srcId = this.getNumberFromLedId((event.target as Element).id);
this.deviceConfigList.led.every(v => v.brightness = this.deviceConfigList.led[srcId].brightness); this.deviceConfigList.led.every((v) => (v.brightness = this.deviceConfigList.led[srcId].brightness));
} },
}, },
}); });
</script> </script>

View File

@ -6,15 +6,23 @@
<form @submit="saveDtuConfig"> <form @submit="saveDtuConfig">
<CardElement :text="$t('dtuadmin.DtuConfiguration')" textVariant="text-bg-primary"> <CardElement :text="$t('dtuadmin.DtuConfiguration')" textVariant="text-bg-primary">
<InputElement :label="$t('dtuadmin.Serial')" <InputElement
v-model="dtuConfigList.serial" :label="$t('dtuadmin.Serial')"
type="number" min="1" max="199999999999" v-model="dtuConfigList.serial"
:tooltip="$t('dtuadmin.SerialHint')"/> type="number"
min="1"
max="199999999999"
:tooltip="$t('dtuadmin.SerialHint')"
/>
<InputElement :label="$t('dtuadmin.PollInterval')" <InputElement
v-model="dtuConfigList.pollinterval" :label="$t('dtuadmin.PollInterval')"
type="number" min="1" max="86400" v-model="dtuConfigList.pollinterval"
:postfix="$t('dtuadmin.Seconds')"/> type="number"
min="1"
max="86400"
:postfix="$t('dtuadmin.Seconds')"
/>
<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">
@ -37,11 +45,16 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="range" class="form-control form-range" <input
type="range"
class="form-control form-range"
v-model="dtuConfigList.cmt_palevel" v-model="dtuConfigList.cmt_palevel"
min="-10" max="20" min="-10"
id="inputCmtPaLevel" aria-describedby="basic-addon1" max="20"
style="height: unset;" /> id="inputCmtPaLevel"
aria-describedby="basic-addon1"
style="height: unset"
/>
<span class="input-group-text" id="basic-addon1">{{ cmtPaLevelText }}</span> <span class="input-group-text" id="basic-addon1">{{ cmtPaLevelText }}</span>
</div> </div>
</div> </div>
@ -55,7 +68,12 @@
<div class="col-sm-10"> <div class="col-sm-10">
<select id="inputCmtCountry" class="form-select" v-model="dtuConfigList.cmt_country"> <select id="inputCmtCountry" class="form-select" v-model="dtuConfigList.cmt_country">
<option v-for="(country, index) in dtuConfigList.country_def" :key="index" :value="index"> <option v-for="(country, index) in dtuConfigList.country_def" :key="index" :value="index">
{{ $t(`dtuadmin.country_` + index, {min: country.freq_min / 1e6, max: country.freq_max / 1e6}) }} {{
$t(`dtuadmin.country_` + index, {
min: country.freq_min / 1e6,
max: country.freq_max / 1e6,
})
}}
</option> </option>
</select> </select>
</div> </div>
@ -68,30 +86,40 @@
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group mb-3"> <div class="input-group mb-3">
<input type="range" class="form-control form-range" <input
type="range"
class="form-control form-range"
v-model="dtuConfigList.cmt_frequency" v-model="dtuConfigList.cmt_frequency"
:min="cmtMinFrequency" :max="cmtMaxFrequency" :step="dtuConfigList.cmt_chan_width" :min="cmtMinFrequency"
id="cmtFrequency" aria-describedby="basic-addon2" :max="cmtMaxFrequency"
style="height: unset;" /> :step="dtuConfigList.cmt_chan_width"
id="cmtFrequency"
aria-describedby="basic-addon2"
style="height: unset"
/>
<span class="input-group-text" id="basic-addon2">{{ cmtFrequencyText }}</span> <span class="input-group-text" id="basic-addon2">{{ cmtFrequencyText }}</span>
</div> </div>
<div class="alert alert-danger" role="alert" v-html="$t('dtuadmin.CmtFrequencyWarning')" v-if="cmtIsOutOfLegalRange"></div> <div
class="alert alert-danger"
role="alert"
v-html="$t('dtuadmin.CmtFrequencyWarning')"
v-if="cmtIsOutOfLegalRange"
></div>
</div> </div>
</div> </div>
</CardElement> </CardElement>
<FormFooter @reload="getDtuConfig"/> <FormFooter @reload="getDtuConfig" />
</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 { DtuConfig } from "@/types/DtuConfig"; import type { DtuConfig } from '@/types/DtuConfig';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { BIconInfoCircle } from 'bootstrap-icons-vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -110,13 +138,13 @@ export default defineComponent({
dataLoading: true, dataLoading: true,
dtuConfigList: {} as DtuConfig, dtuConfigList: {} as DtuConfig,
nrfpalevelList: [ nrfpalevelList: [
{ key: 0, value: 'Min', db: "-18" }, { key: 0, value: 'Min', db: '-18' },
{ key: 1, value: 'Low', db: "-12" }, { key: 1, value: 'Low', db: '-12' },
{ key: 2, value: 'High', db: "-6" }, { key: 2, value: 'High', db: '-6' },
{ key: 3, value: 'Max', db: "0" }, { key: 3, value: 'Max', db: '0' },
], ],
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
}; };
}, },
@ -125,10 +153,12 @@ export default defineComponent({
}, },
computed: { computed: {
cmtFrequencyText() { cmtFrequencyText() {
return this.$t("dtuadmin.MHz", { mhz: this.$n(this.dtuConfigList.cmt_frequency / 1000000, "decimalTwoDigits") }); return this.$t('dtuadmin.MHz', {
mhz: this.$n(this.dtuConfigList.cmt_frequency / 1000000, 'decimalTwoDigits'),
});
}, },
cmtPaLevelText() { cmtPaLevelText() {
return this.$t("dtuadmin.dBm", { dbm: this.$n(this.dtuConfigList.cmt_palevel * 1) }); return this.$t('dtuadmin.dBm', { dbm: this.$n(this.dtuConfigList.cmt_palevel * 1) });
}, },
cmtMinFrequency() { cmtMinFrequency() {
return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_min; return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_min;
@ -137,9 +167,13 @@ export default defineComponent({
return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_max; return this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_max;
}, },
cmtIsOutOfLegalRange() { cmtIsOutOfLegalRange() {
return this.dtuConfigList.cmt_frequency < this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_min return (
|| this.dtuConfigList.cmt_frequency > this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_max; this.dtuConfigList.cmt_frequency <
} this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_min ||
this.dtuConfigList.cmt_frequency >
this.dtuConfigList.country_def[this.dtuConfigList.cmt_country].freq_legal_max
);
},
}, },
watch: { watch: {
'dtuConfigList.cmt_country'(newValue, oldValue) { 'dtuConfigList.cmt_country'(newValue, oldValue) {
@ -149,39 +183,35 @@ export default defineComponent({
this.dtuConfigList.cmt_frequency = this.dtuConfigList.country_def[newValue].freq_default; this.dtuConfigList.cmt_frequency = this.dtuConfigList.country_def[newValue].freq_default;
}); });
} }
} },
}, },
methods: { methods: {
getDtuConfig() { getDtuConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/dtu/config", { headers: authHeader() }) fetch('/api/dtu/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((data) => {
(data) => { this.dtuConfigList = data;
this.dtuConfigList = data; this.dataLoading = false;
this.dataLoading = false; });
}
);
}, },
saveDtuConfig(e: Event) { saveDtuConfig(e: Event) {
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.dtuConfigList)); formData.append('data', JSON.stringify(this.dtuConfigList));
fetch("/api/dtu/config", { fetch('/api/dtu/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

@ -8,8 +8,11 @@
</div> </div>
</div> </div>
<CardElement :text="$t('firmwareupgrade.OtaError')" textVariant="text-bg-danger" center-content <CardElement
v-if="!loading && !uploading && OTAError != ''" :text="$t('firmwareupgrade.OtaError')"
textVariant="text-bg-danger"
center-content
v-if="!loading && !uploading && OTAError != ''"
> >
<p class="h1 mb-2"> <p class="h1 mb-2">
<BIconExclamationCircleFill /> <BIconExclamationCircleFill />
@ -20,16 +23,17 @@
</span> </span>
<br /> <br />
<br /> <br />
<button class="btn btn-light" @click="clear"> <button class="btn btn-light" @click="clear"><BIconArrowLeft /> {{ $t('firmwareupgrade.Back') }}</button>
<BIconArrowLeft /> {{ $t('firmwareupgrade.Back') }}
</button>
<button class="btn btn-primary" @click="retryOTA"> <button class="btn btn-primary" @click="retryOTA">
<BIconArrowRepeat /> {{ $t('firmwareupgrade.Retry') }} <BIconArrowRepeat /> {{ $t('firmwareupgrade.Retry') }}
</button> </button>
</CardElement> </CardElement>
<CardElement :text="$t('firmwareupgrade.OtaStatus')" textVariant="text-bg-success" center-content <CardElement
v-else-if="!loading && !uploading && OTASuccess" :text="$t('firmwareupgrade.OtaStatus')"
textVariant="text-bg-success"
center-content
v-else-if="!loading && !uploading && OTASuccess"
> >
<span class="h1 mb-2"> <span class="h1 mb-2">
<BIconCheckCircle /> <BIconCheckCircle />
@ -44,25 +48,36 @@
</div> </div>
</CardElement> </CardElement>
<CardElement :text="$t('firmwareupgrade.FirmwareUpload')" textVariant="text-bg-primary" center-content <CardElement
v-else-if="!loading && !uploading" :text="$t('firmwareupgrade.FirmwareUpload')"
textVariant="text-bg-primary"
center-content
v-else-if="!loading && !uploading"
> >
<div class="form-group pt-2 mt-3"> <div class="form-group pt-2 mt-3">
<input class="form-control" type="file" ref="file" accept=".bin,.bin.gz" @change="uploadOTA" /> <input class="form-control" type="file" ref="file" accept=".bin,.bin.gz" @change="uploadOTA" />
</div> </div>
</CardElement> </CardElement>
<CardElement :text="$t('firmwareupgrade.UploadProgress')" textVariant="text-bg-primary" center-content <CardElement
v-else-if="!loading && uploading" :text="$t('firmwareupgrade.UploadProgress')"
textVariant="text-bg-primary"
center-content
v-else-if="!loading && uploading"
> >
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" :style="{ width: progress + '%' }" <div
v-bind:aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100"> class="progress-bar"
role="progressbar"
:style="{ width: progress + '%' }"
v-bind:aria-valuenow="progress"
aria-valuemin="0"
aria-valuemax="100"
>
{{ progress }}% {{ progress }}%
</div> </div>
</div> </div>
</CardElement> </CardElement>
</BasePage> </BasePage>
</template> </template>
@ -70,13 +85,8 @@
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 { authHeader, isLoggedIn } from '@/utils/authentication'; import { authHeader, isLoggedIn } from '@/utils/authentication';
import { import { BIconArrowLeft, BIconArrowRepeat, BIconCheckCircle, BIconExclamationCircleFill } from 'bootstrap-icons-vue';
BIconArrowLeft, import SparkMD5 from 'spark-md5';
BIconArrowRepeat,
BIconCheckCircle,
BIconExclamationCircleFill
} from 'bootstrap-icons-vue';
import SparkMD5 from "spark-md5";
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
@ -93,11 +103,11 @@ export default defineComponent({
loading: true, loading: true,
uploading: false, uploading: false,
progress: 0, progress: 0,
OTAError: "", OTAError: '',
OTASuccess: false, OTASuccess: false,
type: "firmware", type: 'firmware',
file: {} as Blob, file: {} as Blob,
hostCheckInterval: 0 hostCheckInterval: 0,
}; };
}, },
methods: { methods: {
@ -124,8 +134,7 @@ export default defineComponent({
}; };
const loadNext = () => { const loadNext = () => {
const start = currentChunk * chunkSize; const start = currentChunk * chunkSize;
const end = const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}; };
loadNext(); loadNext();
@ -141,7 +150,7 @@ export default defineComponent({
} }
} }
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
request.addEventListener("load", () => { request.addEventListener('load', () => {
// request.response will hold the response from the server // request.response will hold the response from the server
if (request.status === 200) { if (request.status === 200) {
this.OTASuccess = true; this.OTASuccess = true;
@ -155,56 +164,55 @@ export default defineComponent({
this.progress = 0; this.progress = 0;
}); });
// Upload progress // Upload progress
request.upload.addEventListener("progress", (e) => { request.upload.addEventListener('progress', (e) => {
this.progress = Math.trunc((e.loaded / e.total) * 100); this.progress = Math.trunc((e.loaded / e.total) * 100);
}); });
request.withCredentials = true; request.withCredentials = true;
this.fileMD5(this.file) this.fileMD5(this.file)
.then((md5) => { .then((md5) => {
formData.append("MD5", (md5 as string)); formData.append('MD5', md5 as string);
formData.append("firmware", this.file, "firmware"); formData.append('firmware', this.file, 'firmware');
request.open("post", "/api/firmware/update"); request.open('post', '/api/firmware/update');
authHeader().forEach((value, key) => { authHeader().forEach((value, key) => {
request.setRequestHeader(key, value); request.setRequestHeader(key, value);
}); });
request.send(formData); request.send(formData);
}) })
.catch(() => { .catch(() => {
this.OTAError = this.OTAError = 'Unknown error while upload, check the console for details.';
"Unknown error while upload, check the console for details.";
this.uploading = false; this.uploading = false;
this.progress = 0; this.progress = 0;
}); });
}, },
retryOTA() { retryOTA() {
this.OTAError = ""; this.OTAError = '';
this.OTASuccess = false; this.OTASuccess = false;
this.uploadOTA(null); this.uploadOTA(null);
}, },
clear() { clear() {
this.OTAError = ""; this.OTAError = '';
this.OTASuccess = false; this.OTASuccess = false;
}, },
checkRemoteHostAndReload(): void { checkRemoteHostAndReload(): void {
// Check if the browser is online // Check if the browser is online
if (navigator.onLine) { if (navigator.onLine) {
const remoteHostUrl = "/api/system/status"; const remoteHostUrl = '/api/system/status';
// Use a simple fetch request to check if the remote host is reachable // Use a simple fetch request to check if the remote host is reachable
fetch(remoteHostUrl, { method: 'GET' }) fetch(remoteHostUrl, { method: 'GET' })
.then(response => { .then((response) => {
// Check if the response status is OK (200-299 range) // Check if the response status is OK (200-299 range)
if (response.ok) { if (response.ok) {
console.log('Remote host is available. Reloading page...'); console.log('Remote host is available. Reloading page...');
clearInterval(this.hostCheckInterval); clearInterval(this.hostCheckInterval);
this.hostCheckInterval = 0; this.hostCheckInterval = 0;
// Perform a page reload // Perform a page reload
window.location.replace("/"); window.location.replace('/');
} else { } else {
console.log('Remote host is not reachable. Do something else if needed.'); console.log('Remote host is not reachable. Do something else if needed.');
} }
}) })
.catch(error => { .catch((error) => {
console.error('Error checking remote host:', error); console.error('Error checking remote host:', error);
}); });
} else { } else {
@ -214,12 +222,15 @@ export default defineComponent({
}, },
mounted() { mounted() {
if (!isLoggedIn()) { if (!isLoggedIn()) {
this.$router.push({ path: "/login", query: { returnUrl: this.$router.currentRoute.value.fullPath } }); this.$router.push({
path: '/login',
query: { returnUrl: this.$router.currentRoute.value.fullPath },
});
} }
this.loading = false; this.loading = false;
}, },
unmounted() { unmounted() {
clearInterval(this.hostCheckInterval); clearInterval(this.hostCheckInterval);
} },
}); });
</script> </script>

View File

@ -1,18 +1,36 @@
<template> <template>
<BasePage :title="$t('home.LiveData')" :isLoading="dataLoading" :isWideScreen="true" :showWebSocket="true" :isWebsocketConnected="isWebsocketConnected" @reload="reloadData"> <BasePage
:title="$t('home.LiveData')"
:isLoading="dataLoading"
:isWideScreen="true"
:showWebSocket="true"
:isWebsocketConnected="isWebsocketConnected"
@reload="reloadData"
>
<HintView :hints="liveData.hints" /> <HintView :hints="liveData.hints" />
<InverterTotalInfo :totalData="liveData.total" /><br /> <InverterTotalInfo :totalData="liveData.total" /><br />
<div class="row gy-3"> <div class="row gy-3">
<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">
<button v-for="inverter in inverterData" :key="inverter.serial" class="nav-link border border-primary text-break" <button
:id="'v-pills-' + inverter.serial + '-tab'" data-bs-toggle="pill" v-for="inverter in inverterData"
:data-bs-target="'#v-pills-' + inverter.serial" type="button" role="tab" :key="inverter.serial"
aria-controls="'v-pills-' + inverter.serial" aria-selected="true"> class="nav-link border border-primary text-break"
:id="'v-pills-' + inverter.serial + '-tab'"
data-bs-toggle="pill"
:data-bs-target="'#v-pills-' + inverter.serial"
type="button"
role="tab"
aria-controls="'v-pills-' + inverter.serial"
aria-selected="true"
>
<div class="row"> <div class="row">
<div class="col-auto col-sm-2"> <div class="col-auto col-sm-2">
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" /> <BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" /> <BIconExclamationCircleFill
class="fs-4"
v-if="inverter.reachable && !inverter.producing"
/>
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" /> <BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
</div> </div>
<div class="col-sm-9"> <div class="col-sm-9">
@ -23,36 +41,50 @@
</div> </div>
</div> </div>
<div class="tab-content" id="v-pills-tabContent" :class="{ <div
'col-sm-9 col-md-10': inverterData.length > 1, class="tab-content"
'col-sm-12 col-md-12': inverterData.length == 1 id="v-pills-tabContent"
}"> :class="{
<div v-for="inverter in inverterData" :key="inverter.serial" class="tab-pane fade show" 'col-sm-9 col-md-10': inverterData.length > 1,
:id="'v-pills-' + inverter.serial" role="tabpanel" 'col-sm-12 col-md-12': inverterData.length == 1,
:aria-labelledby="'v-pills-' + inverter.serial + '-tab'" tabindex="0"> }"
>
<div
v-for="inverter in inverterData"
:key="inverter.serial"
class="tab-pane fade show"
:id="'v-pills-' + inverter.serial"
role="tabpanel"
:aria-labelledby="'v-pills-' + inverter.serial + '-tab'"
tabindex="0"
>
<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-tertiary': !inverter.poll_enabled, 'text-bg-tertiary': !inverter.poll_enabled,
'text-bg-danger': inverter.poll_enabled && !inverter.reachable, 'text-bg-danger': inverter.poll_enabled && !inverter.reachable,
'text-bg-warning': inverter.poll_enabled && inverter.reachable && !inverter.producing, 'text-bg-warning': inverter.poll_enabled && inverter.reachable && !inverter.producing,
'text-bg-primary': inverter.poll_enabled && inverter.reachable && inverter.producing, 'text-bg-primary': inverter.poll_enabled && inverter.reachable && inverter.producing,
}"> }"
>
<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">
{{ inverter.name }} {{ inverter.name }}
</div> </div>
<div style="padding-right: 2em;"> <div style="padding-right: 2em">
{{ $t('home.SerialNumber') }}{{ inverter.serial }} {{ $t('home.SerialNumber') }}{{ inverter.serial }}
</div> </div>
<div style="padding-right: 2em;"> <div style="padding-right: 2em">
{{ $t('home.CurrentLimit') }}<template v-if="inverter.limit_absolute > -1"> {{ {{ $t('home.CurrentLimit')
$n(inverter.limit_absolute, 'decimalNoDigits') }}<template v-if="inverter.limit_absolute > -1">
}} W | </template>{{ $n(inverter.limit_relative / 100, 'percent') }} {{ $n(inverter.limit_absolute, 'decimalNoDigits') }} W | </template
>{{ $n(inverter.limit_relative / 100, 'percent') }}
</div> </div>
<div style="padding-right: 2em;"> <div style="padding-right: 2em">
{{ $t('home.DataAge') }} {{ $t('home.Seconds', {'val': $n(inverter.data_age) }) }} {{ $t('home.DataAge') }}
{{ $t('home.Seconds', { val: $n(inverter.data_age) }) }}
<template v-if="inverter.data_age > 300"> <template v-if="inverter.data_age > 300">
/ {{ calculateAbsoluteTime(inverter.data_age) }} / {{ calculateAbsoluteTime(inverter.data_age) }}
</template> </template>
@ -61,44 +93,68 @@
</div> </div>
<div class="btn-toolbar p-2" role="toolbar"> <div class="btn-toolbar p-2" role="toolbar">
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button :disabled="!isLogged" type="button" class="btn btn-sm btn-danger" <button
@click="onShowLimitSettings(inverter.serial)" v-tooltip :title="$t('home.ShowSetInverterLimit')"> :disabled="!isLogged"
<BIconSpeedometer style="font-size:24px;" /> type="button"
class="btn btn-sm btn-danger"
@click="onShowLimitSettings(inverter.serial)"
v-tooltip
:title="$t('home.ShowSetInverterLimit')"
>
<BIconSpeedometer style="font-size: 24px" />
</button> </button>
</div> </div>
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button :disabled="!isLogged" type="button" class="btn btn-sm btn-danger" <button
@click="onShowPowerSettings(inverter.serial)" v-tooltip :title="$t('home.TurnOnOff')"> :disabled="!isLogged"
<BIconPower style="font-size:24px;" /> type="button"
class="btn btn-sm btn-danger"
@click="onShowPowerSettings(inverter.serial)"
v-tooltip
:title="$t('home.TurnOnOff')"
>
<BIconPower style="font-size: 24px" />
</button> </button>
</div> </div>
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button type="button" class="btn btn-sm btn-info" <button
@click="onShowDevInfo(inverter.serial)" v-tooltip :title="$t('home.ShowInverterInfo')"> type="button"
<BIconCpu style="font-size:24px;" /> class="btn btn-sm btn-info"
@click="onShowDevInfo(inverter.serial)"
v-tooltip
:title="$t('home.ShowInverterInfo')"
>
<BIconCpu style="font-size: 24px" />
</button> </button>
</div> </div>
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button type="button" class="btn btn-sm btn-info" <button
@click="onShowGridProfile(inverter.serial)" v-tooltip :title="$t('home.ShowGridProfile')"> type="button"
<BIconOutlet style="font-size:24px;" /> class="btn btn-sm btn-info"
@click="onShowGridProfile(inverter.serial)"
v-tooltip
:title="$t('home.ShowGridProfile')"
>
<BIconOutlet style="font-size: 24px" />
</button> </button>
</div> </div>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button v-if="inverter.events >= 0" type="button" <button
v-if="inverter.events >= 0"
type="button"
class="btn btn-sm btn-secondary position-relative" class="btn btn-sm btn-secondary position-relative"
@click="onShowEventlog(inverter.serial)" v-tooltip :title="$t('home.ShowEventlog')"> @click="onShowEventlog(inverter.serial)"
<BIconJournalText style="font-size:24px;" /> v-tooltip
:title="$t('home.ShowEventlog')"
>
<BIconJournalText style="font-size: 24px" />
<span <span
class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger"> class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger"
>
{{ inverter.events }} {{ inverter.events }}
<span class="visually-hidden">{{ $t('home.UnreadMessages') }}</span> <span class="visually-hidden">{{ $t('home.UnreadMessages') }}</span>
</span> </span>
@ -108,17 +164,37 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row flex-row-reverse flex-wrap-reverse g-3"> <div class="row flex-row-reverse flex-wrap-reverse g-3">
<template v-for="chanType in [{obj: inverter.INV, name: 'INV'}, {obj: inverter.AC, name: 'AC'}, {obj: inverter.DC, name: 'DC'}].reverse()"> <template
v-for="chanType in [
{ obj: inverter.INV, name: 'INV' },
{ obj: inverter.AC, name: 'AC' },
{ obj: inverter.DC, name: 'DC' },
].reverse()"
>
<template v-if="chanType.obj != null"> <template v-if="chanType.obj != null">
<template v-for="channel in Object.keys(chanType.obj).sort().reverse().map(x=>+x)" :key="channel"> <template
<template v-if="(chanType.name != 'DC') || v-for="channel in Object.keys(chanType.obj)
(chanType.name == 'DC' && getSumIrridiation(inverter) == 0) || .sort()
(chanType.name == 'DC' && getSumIrridiation(inverter) > 0 && chanType.obj[channel].Irradiation?.max || 0 > 0) .reverse()
"> .map((x) => +x)"
:key="channel"
>
<template
v-if="
chanType.name != 'DC' ||
(chanType.name == 'DC' && getSumIrridiation(inverter) == 0) ||
(chanType.name == 'DC' &&
getSumIrridiation(inverter) > 0 &&
chanType.obj[channel].Irradiation?.max) ||
0 > 0
"
>
<div class="col"> <div class="col">
<InverterChannelInfo :channelData="chanType.obj[channel]" <InverterChannelInfo
:channelData="chanType.obj[channel]"
:channelType="chanType.name" :channelType="chanType.name"
:channelNumber="channel" /> :channelNumber="channel"
/>
</div> </div>
</template> </template>
</template> </template>
@ -158,20 +234,31 @@
</BootstrapAlert> </BootstrapAlert>
<div class="row mb-3"> <div class="row mb-3">
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">{{ $t('home.CurrentLimit') }} <label for="inputCurrentLimit" class="col-sm-3 col-form-label">{{ $t('home.CurrentLimit') }} </label>
</label>
<div class="col-sm-4"> <div class="col-sm-4">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="inputCurrentLimit" aria-describedby="currentLimitType" <input
v-model="currentLimitRelative" disabled /> type="text"
class="form-control"
id="inputCurrentLimit"
aria-describedby="currentLimitType"
v-model="currentLimitRelative"
disabled
/>
<span class="input-group-text" id="currentLimitType">%</span> <span class="input-group-text" id="currentLimitType">%</span>
</div> </div>
</div> </div>
<div class="col-sm-4" v-if="currentLimitList.max_power > 0"> <div class="col-sm-4" v-if="currentLimitList.max_power > 0">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="inputCurrentLimitAbsolute" <input
aria-describedby="currentLimitTypeAbsolute" v-model="currentLimitAbsolute" disabled /> type="text"
class="form-control"
id="inputCurrentLimitAbsolute"
aria-describedby="currentLimitTypeAbsolute"
v-model="currentLimitAbsolute"
disabled
/>
<span class="input-group-text" id="currentLimitTypeAbsolute">W</span> <span class="input-group-text" id="currentLimitTypeAbsolute">W</span>
</div> </div>
</div> </div>
@ -182,45 +269,67 @@
{{ $t('home.LastLimitSetStatus') }} {{ $t('home.LastLimitSetStatus') }}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-9">
<span class="badge" :class="{ <span
'text-bg-danger': currentLimitList.limit_set_status == 'Failure', class="badge"
'text-bg-warning': currentLimitList.limit_set_status == 'Pending', :class="{
'text-bg-success': currentLimitList.limit_set_status == 'Ok', 'text-bg-danger': currentLimitList.limit_set_status == 'Failure',
'text-bg-secondary': currentLimitList.limit_set_status == 'Unknown', 'text-bg-warning': currentLimitList.limit_set_status == 'Pending',
}"> 'text-bg-success': currentLimitList.limit_set_status == 'Ok',
'text-bg-secondary': currentLimitList.limit_set_status == 'Unknown',
}"
>
{{ $t('home.' + currentLimitList.limit_set_status) }} {{ $t('home.' + currentLimitList.limit_set_status) }}
</span> </span>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<label for="inputTargetLimit" class="col-sm-3 col-form-label">{{ $t('home.SetLimit') <label for="inputTargetLimit" class="col-sm-3 col-form-label">{{ $t('home.SetLimit') }}</label>
}}</label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input type="number" name="inputTargetLimit" class="form-control" id="inputTargetLimit" <input
:min="targetLimitMin" :max="targetLimitMax" v-model="targetLimitList.limit_value"> type="number"
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" name="inputTargetLimit"
aria-expanded="false">{{ targetLimitTypeText class="form-control"
}}</button> id="inputTargetLimit"
:min="targetLimitMin"
:max="targetLimitMax"
v-model="targetLimitList.limit_value"
/>
<button
class="btn btn-primary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ targetLimitTypeText }}
</button>
<ul class="dropdown-menu dropdown-menu-end"> <ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" @click="onSelectType(1)" href="#">{{ <li>
$t('home.Relative') }}</a></li> <a class="dropdown-item" @click="onSelectType(1)" href="#">{{ $t('home.Relative') }}</a>
<li><a class="dropdown-item" @click="onSelectType(0)" href="#">{{ </li>
$t('home.Absolute') }}</a></li> <li>
<a class="dropdown-item" @click="onSelectType(0)" href="#">{{ $t('home.Absolute') }}</a>
</li>
</ul> </ul>
</div> </div>
<div v-if="targetLimitType == 0" class="alert alert-secondary mt-3" role="alert" <div
v-html="$t('home.LimitHint')"></div> v-if="targetLimitType == 0"
class="alert alert-secondary mt-3"
role="alert"
v-html="$t('home.LimitHint')"
></div>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<button type="button" class="btn btn-danger" @click="onSetLimitSettings(true)">{{ <button type="button" class="btn btn-danger" @click="onSetLimitSettings(true)">
$t('home.SetPersistent') }}</button> {{ $t('home.SetPersistent') }}
</button>
<button type="button" class="btn btn-danger" @click="onSetLimitSettings(false)">{{ <button type="button" class="btn btn-danger" @click="onSetLimitSettings(false)">
$t('home.SetNonPersistent') }}</button> {{ $t('home.SetNonPersistent') }}
</button>
</template> </template>
</ModalDialog> </ModalDialog>
@ -230,15 +339,17 @@
</BootstrapAlert> </BootstrapAlert>
<div class="row mb-3 align-items-center"> <div class="row mb-3 align-items-center">
<label for="inputLastPowerSet" class="col col-form-label">{{ $t('home.LastPowerSetStatus') <label for="inputLastPowerSet" class="col col-form-label">{{ $t('home.LastPowerSetStatus') }}</label>
}}</label>
<div class="col"> <div class="col">
<span class="badge" :class="{ <span
class="badge"
:class="{
'text-bg-danger': successCommandPower == 'Failure', 'text-bg-danger': successCommandPower == 'Failure',
'text-bg-warning': successCommandPower == 'Pending', 'text-bg-warning': successCommandPower == 'Pending',
'text-bg-success': successCommandPower == 'Ok', 'text-bg-success': successCommandPower == 'Ok',
'text-bg-secondary': successCommandPower == 'Unknown', 'text-bg-secondary': successCommandPower == 'Unknown',
}"> }"
>
{{ $t('home.' + successCommandPower) }} {{ $t('home.' + successCommandPower) }}
</span> </span>
</div> </div>
@ -265,7 +376,7 @@ import DevInfo from '@/components/DevInfo.vue';
import EventLog from '@/components/EventLog.vue'; import EventLog from '@/components/EventLog.vue';
import GridProfile from '@/components/GridProfile.vue'; import GridProfile from '@/components/GridProfile.vue';
import HintView from '@/components/HintView.vue'; import HintView from '@/components/HintView.vue';
import InverterChannelInfo from "@/components/InverterChannelInfo.vue"; 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 type { DevInfoStatus } from '@/types/DevInfoStatus'; import type { DevInfoStatus } from '@/types/DevInfoStatus';
@ -288,7 +399,7 @@ import {
BIconSpeedometer, BIconSpeedometer,
BIconToggleOff, BIconToggleOff,
BIconToggleOn, BIconToggleOn,
BIconXCircleFill BIconXCircleFill,
} from 'bootstrap-icons-vue'; } from 'bootstrap-icons-vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -347,17 +458,17 @@ export default defineComponent({
targetLimitTypeText: this.$t('home.Relative'), targetLimitTypeText: this.$t('home.Relative'),
targetLimitType: 1, targetLimitType: 1,
alertMessageLimit: "", alertMessageLimit: '',
alertTypeLimit: "info", alertTypeLimit: 'info',
showAlertLimit: false, showAlertLimit: false,
powerSettingView: {} as bootstrap.Modal, powerSettingView: {} as bootstrap.Modal,
powerSettingSerial: "", powerSettingSerial: '',
powerSettingLoading: true, powerSettingLoading: true,
alertMessagePower: "", alertMessagePower: '',
alertTypePower: "info", alertTypePower: 'info',
showAlertPower: false, showAlertPower: false,
successCommandPower: "", successCommandPower: '',
isWebsocketConnected: false, isWebsocketConnected: false,
}; };
@ -366,10 +477,10 @@ export default defineComponent({
this.getInitialData(); this.getInitialData();
this.initSocket(); this.initSocket();
this.initDataAgeing(); this.initDataAgeing();
this.$emitter.on("logged-in", () => { this.$emitter.on('logged-in', () => {
this.isLogged = this.isLoggedIn(); this.isLogged = this.isLoggedIn();
}); });
this.$emitter.on("logged-out", () => { this.$emitter.on('logged-out', () => {
this.isLogged = this.isLoggedIn(); this.isLogged = this.isLoggedIn();
}); });
}, },
@ -384,19 +495,17 @@ export default defineComponent({
this.closeSocket(); this.closeSocket();
}, },
updated() { updated() {
console.log("Updated"); console.log('Updated');
// Select first tab // Select first tab
if (this.isFirstFetchAfterConnect) { if (this.isFirstFetchAfterConnect) {
console.log("isFirstFetchAfterConnect"); console.log('isFirstFetchAfterConnect');
this.$nextTick(() => { this.$nextTick(() => {
console.log("nextTick"); console.log('nextTick');
const firstTabEl = document.querySelector( const firstTabEl = document.querySelector('#v-pills-tab:first-child button');
"#v-pills-tab:first-child button"
);
if (firstTabEl != null) { if (firstTabEl != null) {
this.isFirstFetchAfterConnect = false; this.isFirstFetchAfterConnect = false;
console.log("Show"); console.log('Show');
const firstTab = new bootstrap.Tab(firstTabEl); const firstTab = new bootstrap.Tab(firstTabEl);
firstTab.show(); firstTab.show();
} }
@ -406,20 +515,21 @@ export default defineComponent({
computed: { computed: {
currentLimitAbsolute(): string { currentLimitAbsolute(): string {
if (this.currentLimitList.max_power > 0) { if (this.currentLimitList.max_power > 0) {
return this.$n(this.currentLimitList.limit_relative * this.currentLimitList.max_power / 100, return this.$n(
'decimalTwoDigits'); (this.currentLimitList.limit_relative * this.currentLimitList.max_power) / 100,
'decimalTwoDigits'
);
} }
return "0"; return '0';
}, },
currentLimitRelative(): string { currentLimitRelative(): string {
return this.$n(this.currentLimitList.limit_relative, return this.$n(this.currentLimitList.limit_relative, 'decimalTwoDigits');
'decimalTwoDigits');
}, },
inverterData(): Inverter[] { inverterData(): Inverter[] {
return this.liveData.inverters.slice().sort((a: Inverter, b: Inverter) => { return this.liveData.inverters.slice().sort((a: Inverter, b: Inverter) => {
return a.order - b.order; return a.order - b.order;
}); });
} },
}, },
methods: { methods: {
isLoggedIn, isLoggedIn,
@ -427,7 +537,7 @@ export default defineComponent({
if (triggerLoading) { if (triggerLoading) {
this.dataLoading = true; this.dataLoading = true;
} }
fetch("/api/livedata/status", { headers: authHeader() }) fetch('/api/livedata/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.liveData = data; this.liveData = data;
@ -445,23 +555,24 @@ export default defineComponent({
}, 1000); }, 1000);
}, },
initSocket() { initSocket() {
console.log("Starting connection to WebSocket Server"); console.log('Starting connection to 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}/livedata`;
}://${authString}${host}/livedata`;
this.socket = new WebSocket(webSocketUrl); this.socket = new WebSocket(webSocketUrl);
this.socket.onmessage = (event) => { this.socket.onmessage = (event) => {
console.log(event); console.log(event);
if (event.data != "{}") { if (event.data != '{}') {
const newData = JSON.parse(event.data); const newData = JSON.parse(event.data);
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);
const foundIdx = this.liveData.inverters.findIndex((element) => element.serial == newData.inverters[0].serial); const foundIdx = this.liveData.inverters.findIndex(
(element) => element.serial == newData.inverters[0].serial
);
if (foundIdx == -1) { if (foundIdx == -1) {
Object.assign(this.liveData.inverters, newData.inverters); Object.assign(this.liveData.inverters, newData.inverters);
} else { } else {
@ -478,14 +589,14 @@ export default defineComponent({
this.socket.onopen = (event) => { this.socket.onopen = (event) => {
console.log(event); console.log(event);
console.log("Successfully connected to the echo websocket server..."); console.log('Successfully connected to the echo websocket server...');
this.isWebsocketConnected = true; this.isWebsocketConnected = true;
}; };
this.socket.onclose = () => { this.socket.onclose = () => {
console.log("Connection to websocket closed...") console.log('Connection to websocket closed...');
this.isWebsocketConnected = false; this.isWebsocketConnected = false;
} };
// 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
window.onbeforeunload = () => { window.onbeforeunload = () => {
@ -495,7 +606,7 @@ export default defineComponent({
initDataAgeing() { initDataAgeing() {
this.dataAgeInterval = setInterval(() => { this.dataAgeInterval = setInterval(() => {
if (this.inverterData) { if (this.inverterData) {
this.inverterData.forEach(element => { this.inverterData.forEach((element) => {
element.data_age++; element.data_age++;
}); });
} }
@ -507,7 +618,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
} }
@ -521,7 +632,9 @@ export default defineComponent({
}, },
onShowEventlog(serial: string) { onShowEventlog(serial: string) {
this.eventLogLoading = true; this.eventLogLoading = true;
fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() }) fetch('/api/eventlog/status?inv=' + serial + '&locale=' + this.$i18n.locale, {
headers: authHeader(),
})
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.eventLogList = data; this.eventLogList = data;
@ -532,7 +645,7 @@ export default defineComponent({
}, },
onShowDevInfo(serial: string) { onShowDevInfo(serial: string) {
this.devInfoLoading = true; this.devInfoLoading = true;
fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() }) fetch('/api/devinfo/status?inv=' + serial, { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.devInfoList = data; this.devInfoList = data;
@ -544,30 +657,30 @@ export default defineComponent({
}, },
onShowGridProfile(serial: string) { onShowGridProfile(serial: string) {
this.gridProfileLoading = true; this.gridProfileLoading = true;
fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() }) fetch('/api/gridprofile/status?inv=' + serial, { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.gridProfileList = data; this.gridProfileList = data;
fetch("/api/gridprofile/rawdata?inv=" + serial, { headers: authHeader() }) fetch('/api/gridprofile/rawdata?inv=' + serial, { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.gridProfileRawList = data; this.gridProfileRawList = data;
this.gridProfileLoading = false; this.gridProfileLoading = false;
}) });
}); });
this.gridProfileView.show(); this.gridProfileView.show();
}, },
onShowLimitSettings(serial: string) { onShowLimitSettings(serial: string) {
this.showAlertLimit = false; this.showAlertLimit = false;
this.targetLimitList.serial = ""; this.targetLimitList.serial = '';
this.targetLimitList.limit_value = 0; this.targetLimitList.limit_value = 0;
this.targetLimitType = 1; this.targetLimitType = 1;
this.targetLimitTypeText = this.$t('home.Relative'); this.targetLimitTypeText = this.$t('home.Relative');
this.limitSettingLoading = true; this.limitSettingLoading = true;
fetch("/api/limit/status", { headers: authHeader() }) fetch('/api/limit/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.currentLimitList = data[serial]; this.currentLimitList = data[serial];
@ -578,29 +691,27 @@ export default defineComponent({
this.limitSettingView.show(); this.limitSettingView.show();
}, },
onSetLimitSettings(setPersistent: boolean) { onSetLimitSettings(setPersistent: boolean) {
this.targetLimitList.limit_type = (setPersistent ? 256 : 0) + this.targetLimitType this.targetLimitList.limit_type = (setPersistent ? 256 : 0) + this.targetLimitType;
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.targetLimitList)); formData.append('data', JSON.stringify(this.targetLimitList));
console.log(this.targetLimitList); console.log(this.targetLimitList);
fetch("/api/limit/config", { fetch('/api/limit/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) => { if (response.type == 'success') {
if (response.type == "success") { this.limitSettingView.hide();
this.limitSettingView.hide(); } else {
} else { this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param);
this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param); this.alertTypeLimit = response.type;
this.alertTypeLimit = response.type; this.showAlertLimit = true;
this.showAlertLimit = true;
}
} }
) });
}, },
onSelectType(type: number) { onSelectType(type: number) {
if (type == 1) { if (type == 1) {
@ -610,16 +721,16 @@ export default defineComponent({
} else { } else {
this.targetLimitTypeText = this.$t('home.Absolute'); this.targetLimitTypeText = this.$t('home.Absolute');
this.targetLimitMin = 0; this.targetLimitMin = 0;
this.targetLimitMax = (this.currentLimitList.max_power > 0 ? this.currentLimitList.max_power : 2250); this.targetLimitMax = this.currentLimitList.max_power > 0 ? this.currentLimitList.max_power : 2250;
} }
this.targetLimitType = type; this.targetLimitType = type;
}, },
onShowPowerSettings(serial: string) { onShowPowerSettings(serial: string) {
this.showAlertPower = false; this.showAlertPower = false;
this.powerSettingSerial = ""; this.powerSettingSerial = '';
this.powerSettingLoading = true; this.powerSettingLoading = true;
fetch("/api/power/status", { headers: authHeader() }) fetch('/api/power/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.successCommandPower = data[serial].power_set_status; this.successCommandPower = data[serial].power_set_status;
@ -644,27 +755,25 @@ export default defineComponent({
} }
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(data)); formData.append('data', JSON.stringify(data));
console.log(data); console.log(data);
fetch("/api/power/config", { fetch('/api/power/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) => { if (response.type == 'success') {
if (response.type == "success") { this.powerSettingView.hide();
this.powerSettingView.hide(); } else {
} else { this.alertMessagePower = this.$t('apiresponse.' + response.code, response.param);
this.alertMessagePower = this.$t('apiresponse.' + response.code, response.param); this.alertTypePower = response.type;
this.alertTypePower = response.type; this.showAlertPower = true;
this.showAlertPower = true;
}
} }
) });
}, },
calculateAbsoluteTime(lastTime: number): string { calculateAbsoluteTime(lastTime: number): string {
const date = new Date(Date.now() - lastTime * 1000); const date = new Date(Date.now() - lastTime * 1000);
@ -676,7 +785,7 @@ export default defineComponent({
total += inv.DC[key as unknown as number].Irradiation?.max || 0; total += inv.DC[key as unknown as number].Irradiation?.max || 0;
}); });
return total; return total;
} },
}, },
}); });
</script> </script>

View File

@ -12,11 +12,18 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('inverteradmin.Name') }}</label> <label>{{ $t('inverteradmin.Name') }}</label>
<input v-model="newInverterData.name" type="text" class="form-control ml-sm-2 mr-sm-4 my-2" <input
maxlength="31" required /> v-model="newInverterData.name"
type="text"
class="form-control ml-sm-2 mr-sm-4 my-2"
maxlength="31"
required
/>
</div> </div>
<div class="ml-auto text-right"> <div class="ml-auto text-right">
<button type="submit" class="btn btn-primary my-2">{{ $t('inverteradmin.Add') }}</button> <button type="submit" class="btn btn-primary my-2">
{{ $t('inverteradmin.Add') }}
</button>
</div> </div>
<div class="alert alert-secondary" role="alert" v-html="$t('inverteradmin.AddHint')"></div> <div class="alert alert-secondary" role="alert" v-html="$t('inverteradmin.AddHint')"></div>
</form> </form>
@ -39,23 +46,33 @@
<tr v-for="inverter in inverters" v-bind:key="inverter.id" :data-id="inverter.id"> <tr v-for="inverter in inverters" v-bind:key="inverter.id" :data-id="inverter.id">
<td><BIconGripHorizontal class="drag-handle" /></td> <td><BIconGripHorizontal class="drag-handle" /></td>
<td> <td>
<span class="badge" :title="$t('inverteradmin.Receive')" :class="{ <span
'text-bg-warning': !inverter.poll_enable_night, class="badge"
'text-bg-dark': inverter.poll_enable_night,}" :title="$t('inverteradmin.Receive')"
><BIconArrowDown v-if="inverter.poll_enable" /></span> :class="{
'text-bg-warning': !inverter.poll_enable_night,
'text-bg-dark': inverter.poll_enable_night,
}"
><BIconArrowDown v-if="inverter.poll_enable"
/></span>
<span class="badge" :title="$t('inverteradmin.Send')" :class="{ <span
'text-bg-warning': !inverter.command_enable_night, class="badge"
'text-bg-dark': inverter.command_enable_night,}" :title="$t('inverteradmin.Send')"
><BIconArrowUp v-if="inverter.command_enable" /></span> :class="{
'text-bg-warning': !inverter.command_enable_night,
'text-bg-dark': inverter.command_enable_night,
}"
><BIconArrowUp v-if="inverter.command_enable"
/></span>
</td> </td>
<td>{{ inverter.serial }}</td> <td>{{ inverter.serial }}</td>
<td>{{ inverter.name }}</td> <td>{{ inverter.name }}</td>
<td>{{ inverter.type }}</td> <td>{{ inverter.type }}</td>
<td> <td>
<a href="#" class="icon text-danger" :title="$t('inverteradmin.DeleteInverter')"> <a href="#" class="icon text-danger" :title="$t('inverteradmin.DeleteInverter')">
<BIconTrash v-on:click="onOpenModal(modalDelete, inverter)" /> <BIconTrash v-on:click="onOpenModal(modalDelete, inverter)" /> </a
</a>&nbsp; >&nbsp;
<a href="#" class="icon" :title="$t('inverteradmin.EditInverter')"> <a href="#" class="icon" :title="$t('inverteradmin.EditInverter')">
<BIconPencil v-on:click="onOpenModal(modal, inverter)" /> <BIconPencil v-on:click="onOpenModal(modal, inverter)" />
</a> </a>
@ -65,54 +82,122 @@
</table> </table>
</div> </div>
<div class="ml-auto text-right"> <div class="ml-auto text-right">
<button class="btn btn-primary my-2" @click="onSaveOrder()">{{ $t('inverteradmin.SaveOrder') }}</button> <button class="btn btn-primary my-2" @click="onSaveOrder()">
{{ $t('inverteradmin.SaveOrder') }}
</button>
</div> </div>
</CardElement> </CardElement>
</BasePage> </BasePage>
<ModalDialog modalId="inverterEdit" :title="$t('inverteradmin.EditInverter')" :closeText="$t('inverteradmin.Cancel')"> <ModalDialog
modalId="inverterEdit"
:title="$t('inverteradmin.EditInverter')"
:closeText="$t('inverteradmin.Cancel')"
>
<nav> <nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist"> <div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-general-tab" data-bs-toggle="tab" data-bs-target="#nav-general" <button
type="button" role="tab" aria-controls="nav-general" aria-selected="true">{{ class="nav-link active"
$t('inverteradmin.General') id="nav-general-tab"
}}</button> data-bs-toggle="tab"
<button class="nav-link" id="nav-string-tab" data-bs-toggle="tab" data-bs-target="#nav-string" type="button" data-bs-target="#nav-general"
role="tab" aria-controls="nav-string">{{ $t('inverteradmin.String') }}</button> type="button"
<button class="nav-link" id="nav-advanced-tab" data-bs-toggle="tab" data-bs-target="#nav-advanced" role="tab"
type="button" role="tab" aria-controls="nav-advanced">{{ $t('inverteradmin.Advanced') }}</button> aria-controls="nav-general"
aria-selected="true"
>
{{ $t('inverteradmin.General') }}
</button>
<button
class="nav-link"
id="nav-string-tab"
data-bs-toggle="tab"
data-bs-target="#nav-string"
type="button"
role="tab"
aria-controls="nav-string"
>
{{ $t('inverteradmin.String') }}
</button>
<button
class="nav-link"
id="nav-advanced-tab"
data-bs-toggle="tab"
data-bs-target="#nav-advanced"
type="button"
role="tab"
aria-controls="nav-advanced"
>
{{ $t('inverteradmin.Advanced') }}
</button>
</div> </div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-general" role="tabpanel" aria-labelledby="nav-general-tab" <div
tabindex="0"> class="tab-pane fade show active"
id="nav-general"
role="tabpanel"
aria-labelledby="nav-general-tab"
tabindex="0"
>
<div class="mb-3"> <div class="mb-3">
<label for="inverter-serial" class="col-form-label"> <label for="inverter-serial" class="col-form-label">
{{ $t('inverteradmin.InverterSerial') }} {{ $t('inverteradmin.InverterSerial') }}
</label> </label>
<InputSerial v-model="selectedInverterData.serial" id="inverter-serial" /> <InputSerial v-model="selectedInverterData.serial" id="inverter-serial" />
<label for="inverter-name" class="col-form-label">{{ $t('inverteradmin.InverterName') }} <label for="inverter-name" class="col-form-label"
>{{ $t('inverteradmin.InverterName') }}
<BIconInfoCircle v-tooltip :title="$t('inverteradmin.InverterNameHint')" /> <BIconInfoCircle v-tooltip :title="$t('inverteradmin.InverterNameHint')" />
</label> </label>
<input v-model="selectedInverterData.name" type="text" id="inverter-name" class="form-control" <input
maxlength="31" /> v-model="selectedInverterData.name"
type="text"
id="inverter-name"
class="form-control"
maxlength="31"
/>
<CardElement :text="$t('inverteradmin.InverterStatus')" addSpace> <CardElement :text="$t('inverteradmin.InverterStatus')" addSpace>
<InputElement :label="$t('inverteradmin.PollEnable')" v-model="selectedInverterData.poll_enable" <InputElement
type="checkbox" wide /> :label="$t('inverteradmin.PollEnable')"
<InputElement :label="$t('inverteradmin.PollEnableNight')" v-model="selectedInverterData.poll_enable"
v-model="selectedInverterData.poll_enable_night" type="checkbox" wide /> type="checkbox"
<InputElement :label="$t('inverteradmin.CommandEnable')" wide
v-model="selectedInverterData.command_enable" type="checkbox" wide /> />
<InputElement :label="$t('inverteradmin.CommandEnableNight')" <InputElement
v-model="selectedInverterData.command_enable_night" type="checkbox" wide /> :label="$t('inverteradmin.PollEnableNight')"
<div class="alert alert-secondary mt-3" role="alert" v-html="$t('inverteradmin.StatusHint')"> v-model="selectedInverterData.poll_enable_night"
</div> type="checkbox"
wide
/>
<InputElement
:label="$t('inverteradmin.CommandEnable')"
v-model="selectedInverterData.command_enable"
type="checkbox"
wide
/>
<InputElement
:label="$t('inverteradmin.CommandEnableNight')"
v-model="selectedInverterData.command_enable_night"
type="checkbox"
wide
/>
<div
class="alert alert-secondary mt-3"
role="alert"
v-html="$t('inverteradmin.StatusHint')"
></div>
</CardElement> </CardElement>
</div> </div>
</div> </div>
<div class="tab-pane fade show" id="nav-string" role="tabpanel" aria-labelledby="nav-string-tab" tabindex="0"> <div
class="tab-pane fade show"
id="nav-string"
role="tabpanel"
aria-labelledby="nav-string-tab"
tabindex="0"
>
<div v-for="(ch, index) in selectedInverterData.channel" :key="`${index}`"> <div v-for="(ch, index) in selectedInverterData.channel" :key="`${index}`">
<div class="row g-2"> <div class="row g-2">
<div class="col-md"> <div class="col-md">
@ -122,8 +207,13 @@
</label> </label>
<div class="d-flex mb-2"> <div class="d-flex mb-2">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" :id="`inverter-name_${index}`" maxlength="31" <input
v-model="ch.name" /> type="text"
class="form-control"
:id="`inverter-name_${index}`"
maxlength="31"
v-model="ch.name"
/>
</div> </div>
</div> </div>
</div> </div>
@ -136,11 +226,17 @@
</label> </label>
<div class="d-flex mb-2"> <div class="d-flex mb-2">
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0" <input
type="number"
class="form-control"
:id="`inverter-max_${index}`"
min="0"
v-model="ch.max_power" v-model="ch.max_power"
:aria-describedby="`inverter-maxDescription_${index} inverter-customizer`" /> :aria-describedby="`inverter-maxDescription_${index} inverter-customizer`"
<span class="input-group-text" />
:id="`inverter-maxDescription_${index}`">W<sub>p</sub><sup>*</sup></span> <span class="input-group-text" :id="`inverter-maxDescription_${index}`"
>W<sub>p</sub><sup>*</sup></span
>
</div> </div>
</div> </div>
</div> </div>
@ -151,61 +247,104 @@
</label> </label>
<div class="d-flex mb-2"> <div class="d-flex mb-2">
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" :id="`inverter-ytoffset_${index}`" min="0" <input
type="number"
class="form-control"
:id="`inverter-ytoffset_${index}`"
min="0"
v-model="ch.yield_total_offset" v-model="ch.yield_total_offset"
:aria-describedby="`inverter-ytoffsetDescription_${index} inverter-customizer`" /> :aria-describedby="`inverter-ytoffsetDescription_${index} inverter-customizer`"
<span class="input-group-text" :id="`inverter-ytoffsetDescription_${index}`">kWh</span> />
<span class="input-group-text" :id="`inverter-ytoffsetDescription_${index}`"
>kWh</span
>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div :id="`inverter-customizer`" class="form-text" v-html="$t('inverteradmin.InverterHint')"> <div :id="`inverter-customizer`" class="form-text" v-html="$t('inverteradmin.InverterHint')"></div>
</div>
</div> </div>
<div class="tab-pane fade show" id="nav-advanced" role="tabpanel" aria-labelledby="nav-advanced-tab" <div
tabindex="0"> class="tab-pane fade show"
<InputElement :label="$t('inverteradmin.ReachableThreshold')" id="nav-advanced"
v-model="selectedInverterData.reachable_threshold" type="number" min="1" max="100" role="tabpanel"
:tooltip="$t('inverteradmin.ReachableThresholdHint')" wide /> aria-labelledby="nav-advanced-tab"
tabindex="0"
>
<InputElement
:label="$t('inverteradmin.ReachableThreshold')"
v-model="selectedInverterData.reachable_threshold"
type="number"
min="1"
max="100"
:tooltip="$t('inverteradmin.ReachableThresholdHint')"
wide
/>
<InputElement :label="$t('inverteradmin.ZeroRuntime')" v-model="selectedInverterData.zero_runtime" <InputElement
type="checkbox" :tooltip="$t('inverteradmin.ZeroRuntimeHint')" wide /> :label="$t('inverteradmin.ZeroRuntime')"
v-model="selectedInverterData.zero_runtime"
type="checkbox"
:tooltip="$t('inverteradmin.ZeroRuntimeHint')"
wide
/>
<InputElement :label="$t('inverteradmin.ZeroDay')" v-model="selectedInverterData.zero_day" type="checkbox" <InputElement
:tooltip="$t('inverteradmin.ZeroDayHint')" wide /> :label="$t('inverteradmin.ZeroDay')"
v-model="selectedInverterData.zero_day"
type="checkbox"
:tooltip="$t('inverteradmin.ZeroDayHint')"
wide
/>
<InputElement :label="$t('inverteradmin.ClearEventlog')" v-model="selectedInverterData.clear_eventlog" type="checkbox" wide /> <InputElement
:label="$t('inverteradmin.ClearEventlog')"
v-model="selectedInverterData.clear_eventlog"
type="checkbox"
wide
/>
<InputElement :label="$t('inverteradmin.YieldDayCorrection')" <InputElement
v-model="selectedInverterData.yieldday_correction" type="checkbox" :label="$t('inverteradmin.YieldDayCorrection')"
:tooltip="$t('inverteradmin.YieldDayCorrectionHint')" wide /> v-model="selectedInverterData.yieldday_correction"
type="checkbox"
:tooltip="$t('inverteradmin.YieldDayCorrectionHint')"
wide
/>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<button type="button" class="btn btn-primary" @click="onEditSubmit"> <button type="button" class="btn btn-primary" @click="onEditSubmit">
{{ $t('inverteradmin.Save') }}</button> {{ $t('inverteradmin.Save') }}
</button>
</template> </template>
</ModalDialog> </ModalDialog>
<ModalDialog modalId="inverterDelete" small :title="$t('inverteradmin.DeleteInverter')" <ModalDialog
:closeText="$t('inverteradmin.Cancel')"> modalId="inverterDelete"
{{ $t('inverteradmin.DeleteMsg', { small
name: selectedInverterData.name, :title="$t('inverteradmin.DeleteInverter')"
serial: selectedInverterData.serial :closeText="$t('inverteradmin.Cancel')"
}) >
{{
$t('inverteradmin.DeleteMsg', {
name: selectedInverterData.name,
serial: selectedInverterData.serial,
})
}} }}
<template #footer> <template #footer>
<button type="button" class="btn btn-danger" @click="onDelete"> <button type="button" class="btn btn-danger" @click="onDelete">
{{ $t('inverteradmin.Delete') }}</button> {{ $t('inverteradmin.Delete') }}
</button>
</template> </template>
</ModalDialog> </ModalDialog>
</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 InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import InputSerial from '@/components/InputSerial.vue'; import InputSerial from '@/components/InputSerial.vue';
@ -268,7 +407,7 @@ export default defineComponent({
methods: { methods: {
getInverters() { getInverters() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/inverter/list", { headers: authHeader() }) fetch('/api/inverter/list', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.inverters = data.inverter.slice().sort((a: Inverter, b: Inverter) => { this.inverters = data.inverter.slice().sort((a: Inverter, b: Inverter) => {
@ -290,10 +429,10 @@ export default defineComponent({
}, },
callInverterApiEndpoint(endpoint: string, jsonData: string) { callInverterApiEndpoint(endpoint: string, jsonData: string) {
const formData = new FormData(); const formData = new FormData();
formData.append("data", jsonData); formData.append('data', jsonData);
fetch("/api/inverter/" + endpoint, { fetch('/api/inverter/' + endpoint, {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
@ -306,15 +445,15 @@ export default defineComponent({
}); });
}, },
onSubmit() { onSubmit() {
this.callInverterApiEndpoint("add", JSON.stringify(this.newInverterData)); this.callInverterApiEndpoint('add', JSON.stringify(this.newInverterData));
this.newInverterData = {} as Inverter; this.newInverterData = {} as Inverter;
}, },
onDelete() { onDelete() {
this.callInverterApiEndpoint("del", JSON.stringify({ id: this.selectedInverterData.id })); this.callInverterApiEndpoint('del', JSON.stringify({ id: this.selectedInverterData.id }));
this.onCloseModal(this.modalDelete); this.onCloseModal(this.modalDelete);
}, },
onEditSubmit() { onEditSubmit() {
this.callInverterApiEndpoint("edit", JSON.stringify(this.selectedInverterData)); this.callInverterApiEndpoint('edit', JSON.stringify(this.selectedInverterData));
this.onCloseModal(this.modal); this.onCloseModal(this.modal);
}, },
onOpenModal(modal: bootstrap.Modal, inverter: Inverter) { onOpenModal(modal: bootstrap.Modal, inverter: Inverter) {
@ -326,7 +465,7 @@ export default defineComponent({
modal.hide(); modal.hide();
}, },
onSaveOrder() { onSaveOrder() {
this.callInverterApiEndpoint("order", JSON.stringify({ order: this.sortable.toArray() })); this.callInverterApiEndpoint('order', JSON.stringify({ order: this.sortable.toArray() }));
}, },
}, },
}); });

View File

@ -8,20 +8,35 @@
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<div class="form-group"> <div class="form-group">
<label for="username">{{ $t('login.Username') }}</label> <label for="username">{{ $t('login.Username') }}</label>
<input type="text" v-model="username" name="username" class="form-control" <input
:class="{ 'is-invalid': submitted && !username }" @keydown.space.prevent /> type="text"
<div v-show="submitted && !username" class="invalid-feedback">{{ $t('login.UsernameRequired') }} v-model="username"
name="username"
class="form-control"
:class="{ 'is-invalid': submitted && !username }"
@keydown.space.prevent
/>
<div v-show="submitted && !username" class="invalid-feedback">
{{ $t('login.UsernameRequired') }}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label htmlFor="password">{{ $t('login.Password') }}</label> <label htmlFor="password">{{ $t('login.Password') }}</label>
<input type="password" v-model="password" name="password" class="form-control" <input
:class="{ 'is-invalid': submitted && !password }" /> type="password"
v-model="password"
name="password"
class="form-control"
:class="{ 'is-invalid': submitted && !password }"
/>
<div v-show="submitted && !password" class="invalid-feedback"> <div v-show="submitted && !password" class="invalid-feedback">
{{ $t('login.PasswordRequired') }}</div> {{ $t('login.PasswordRequired') }}
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<button class="btn btn-primary" :disabled="dataLoading">{{ $t('login.LoginButton') }}</button> <button class="btn btn-primary" :disabled="dataLoading">
{{ $t('login.LoginButton') }}
</button>
</div> </div>
</form> </form>
</CardElement> </CardElement>
@ -30,7 +45,7 @@
<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 router from '@/router'; import router from '@/router';
import { login } from '@/utils'; import { login } from '@/utils';
@ -45,8 +60,8 @@ export default defineComponent({
data() { data() {
return { return {
dataLoading: false, dataLoading: false,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
returnUrl: '', returnUrl: '',
username: '', username: '',
@ -69,21 +84,20 @@ export default defineComponent({
} }
this.dataLoading = true; this.dataLoading = true;
login(username, password) login(username, password).then(
.then( () => {
() => { this.$emitter.emit('logged-in');
this.$emitter.emit("logged-in"); router.push(this.returnUrl);
router.push(this.returnUrl); },
}, (error) => {
error => { this.$emitter.emit('logged-out');
this.$emitter.emit("logged-out"); this.alertMessage = error;
this.alertMessage = error; this.alertType = 'danger';
this.alertType = 'danger'; this.showAlert = true;
this.showAlert = true; this.dataLoading = false;
this.dataLoading = false; }
} );
) },
} },
}
}); });
</script> </script>

View File

@ -5,25 +5,32 @@
</BootstrapAlert> </BootstrapAlert>
<CardElement :text="$t('maintenancereboot.PerformReboot')" textVariant="text-bg-primary" center-content> <CardElement :text="$t('maintenancereboot.PerformReboot')" textVariant="text-bg-primary" center-content>
<button class="btn btn-danger" @click="onOpenModal(performReboot)">{{ $t('maintenancereboot.Reboot') }} <button class="btn btn-danger" @click="onOpenModal(performReboot)">
{{ $t('maintenancereboot.Reboot') }}
</button> </button>
<div class="alert alert-danger mt-3" role="alert" v-html="$t('maintenancereboot.RebootHint')"></div> <div class="alert alert-danger mt-3" role="alert" v-html="$t('maintenancereboot.RebootHint')"></div>
</CardElement> </CardElement>
</BasePage> </BasePage>
<ModalDialog modalId="performReboot" small :title="$t('maintenancereboot.RebootOpenDTU')" :closeText="$t('maintenancereboot.Cancel')"> <ModalDialog
modalId="performReboot"
small
:title="$t('maintenancereboot.RebootOpenDTU')"
:closeText="$t('maintenancereboot.Cancel')"
>
{{ $t('maintenancereboot.RebootQuestion') }} {{ $t('maintenancereboot.RebootQuestion') }}
<template #footer> <template #footer>
<button type="button" class="btn btn-danger" @click="onReboot"> <button type="button" class="btn btn-danger" @click="onReboot">
{{ $t('maintenancereboot.Reboot') }}</button> {{ $t('maintenancereboot.Reboot') }}
</button>
</template> </template>
</ModalDialog> </ModalDialog>
</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 ModalDialog from '@/components/ModalDialog.vue'; import ModalDialog from '@/components/ModalDialog.vue';
import { authHeader, handleResponse, isLoggedIn } from '@/utils/authentication'; import { authHeader, handleResponse, isLoggedIn } from '@/utils/authentication';
@ -43,14 +50,17 @@ export default defineComponent({
dataLoading: false, dataLoading: false,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
}; };
}, },
mounted() { mounted() {
if (!isLoggedIn()) { if (!isLoggedIn()) {
this.$router.push({ path: "/login", query: { returnUrl: this.$router.currentRoute.value.fullPath } }); this.$router.push({
path: '/login',
query: { returnUrl: this.$router.currentRoute.value.fullPath },
});
} }
this.performReboot = new bootstrap.Modal('#performReboot'); this.performReboot = new bootstrap.Modal('#performReboot');
@ -58,10 +68,10 @@ export default defineComponent({
methods: { methods: {
onReboot() { onReboot() {
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify({ reboot: true })); formData.append('data', JSON.stringify({ reboot: true }));
fetch("/api/maintenance/reboot", { fetch('/api/maintenance/reboot', {
method: "POST", method: 'POST',
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
}) })
@ -78,7 +88,7 @@ export default defineComponent({
}, },
onCloseModal(modal: bootstrap.Modal) { onCloseModal(modal: bootstrap.Modal) {
modal.hide(); modal.hide();
} },
}, },
}); });
</script> </script>

View File

@ -6,102 +6,163 @@
<form @submit="saveMqttConfig"> <form @submit="saveMqttConfig">
<CardElement :text="$t('mqttadmin.MqttConfiguration')" textVariant="text-bg-primary"> <CardElement :text="$t('mqttadmin.MqttConfiguration')" textVariant="text-bg-primary">
<InputElement :label="$t('mqttadmin.EnableMqtt')" <InputElement
v-model="mqttConfigList.mqtt_enabled" :label="$t('mqttadmin.EnableMqtt')"
type="checkbox" wide/> v-model="mqttConfigList.mqtt_enabled"
type="checkbox"
wide
/>
<InputElement v-show="mqttConfigList.mqtt_enabled" <InputElement
:label="$t('mqttadmin.EnableHass')" v-show="mqttConfigList.mqtt_enabled"
v-model="mqttConfigList.mqtt_hass_enabled" :label="$t('mqttadmin.EnableHass')"
type="checkbox" wide/> v-model="mqttConfigList.mqtt_hass_enabled"
type="checkbox"
wide
/>
</CardElement> </CardElement>
<CardElement :text="$t('mqttadmin.MqttBrokerParameter')" textVariant="text-bg-primary" add-space <CardElement
v-show="mqttConfigList.mqtt_enabled" :text="$t('mqttadmin.MqttBrokerParameter')"
textVariant="text-bg-primary"
add-space
v-show="mqttConfigList.mqtt_enabled"
> >
<InputElement :label="$t('mqttadmin.Hostname')" <InputElement
v-model="mqttConfigList.mqtt_hostname" :label="$t('mqttadmin.Hostname')"
type="text" maxlength="128" v-model="mqttConfigList.mqtt_hostname"
:placeholder="$t('mqttadmin.HostnameHint')"/> type="text"
maxlength="128"
:placeholder="$t('mqttadmin.HostnameHint')"
/>
<InputElement :label="$t('mqttadmin.Port')" <InputElement
v-model="mqttConfigList.mqtt_port" :label="$t('mqttadmin.Port')"
type="number" min="1" max="65535"/> v-model="mqttConfigList.mqtt_port"
type="number"
min="1"
max="65535"
/>
<InputElement :label="$t('mqttadmin.ClientId')" <InputElement
v-model="mqttConfigList.mqtt_clientid" :label="$t('mqttadmin.ClientId')"
type="text" maxlength="64"/> v-model="mqttConfigList.mqtt_clientid"
type="text"
maxlength="64"
/>
<InputElement :label="$t('mqttadmin.Username')" <InputElement
v-model="mqttConfigList.mqtt_username" :label="$t('mqttadmin.Username')"
type="text" maxlength="64" v-model="mqttConfigList.mqtt_username"
:placeholder="$t('mqttadmin.UsernameHint')"/> type="text"
maxlength="64"
:placeholder="$t('mqttadmin.UsernameHint')"
/>
<InputElement :label="$t('mqttadmin.Password')" <InputElement
v-model="mqttConfigList.mqtt_password" :label="$t('mqttadmin.Password')"
type="password" maxlength="64" v-model="mqttConfigList.mqtt_password"
:placeholder="$t('mqttadmin.PasswordHint')"/> type="password"
maxlength="64"
:placeholder="$t('mqttadmin.PasswordHint')"
/>
<InputElement :label="$t('mqttadmin.BaseTopic')" <InputElement
v-model="mqttConfigList.mqtt_topic" :label="$t('mqttadmin.BaseTopic')"
type="text" maxlength="32" v-model="mqttConfigList.mqtt_topic"
:placeholder="$t('mqttadmin.BaseTopicHint')"/> type="text"
maxlength="32"
:placeholder="$t('mqttadmin.BaseTopicHint')"
/>
<InputElement :label="$t('mqttadmin.PublishInterval')" <InputElement
v-model="mqttConfigList.mqtt_publish_interval" :label="$t('mqttadmin.PublishInterval')"
type="number" min="5" max="86400" v-model="mqttConfigList.mqtt_publish_interval"
:postfix="$t('mqttadmin.Seconds')"/> type="number"
min="5"
max="86400"
:postfix="$t('mqttadmin.Seconds')"
/>
<InputElement :label="$t('mqttadmin.CleanSession')" <InputElement
v-model="mqttConfigList.mqtt_clean_session" :label="$t('mqttadmin.CleanSession')"
type="checkbox"/> v-model="mqttConfigList.mqtt_clean_session"
type="checkbox"
/>
<InputElement :label="$t('mqttadmin.EnableRetain')" <InputElement
v-model="mqttConfigList.mqtt_retain" :label="$t('mqttadmin.EnableRetain')"
type="checkbox"/> v-model="mqttConfigList.mqtt_retain"
type="checkbox"
/>
<InputElement :label="$t('mqttadmin.EnableTls')" <InputElement :label="$t('mqttadmin.EnableTls')" v-model="mqttConfigList.mqtt_tls" type="checkbox" />
v-model="mqttConfigList.mqtt_tls"
type="checkbox"/>
<InputElement v-show="mqttConfigList.mqtt_tls" <InputElement
:label="$t('mqttadmin.RootCa')" v-show="mqttConfigList.mqtt_tls"
v-model="mqttConfigList.mqtt_root_ca_cert" :label="$t('mqttadmin.RootCa')"
type="textarea" maxlength="2560" rows="10"/> v-model="mqttConfigList.mqtt_root_ca_cert"
type="textarea"
maxlength="2560"
rows="10"
/>
<InputElement v-show="mqttConfigList.mqtt_tls" <InputElement
:label="$t('mqttadmin.TlsCertLoginEnable')" v-show="mqttConfigList.mqtt_tls"
v-model="mqttConfigList.mqtt_tls_cert_login" :label="$t('mqttadmin.TlsCertLoginEnable')"
type="checkbox"/> v-model="mqttConfigList.mqtt_tls_cert_login"
type="checkbox"
/>
<InputElement v-show="mqttConfigList.mqtt_tls_cert_login" <InputElement
:label="$t('mqttadmin.ClientCert')" v-show="mqttConfigList.mqtt_tls_cert_login"
v-model="mqttConfigList.mqtt_client_cert" :label="$t('mqttadmin.ClientCert')"
type="textarea" maxlength="2560" rows="10"/> v-model="mqttConfigList.mqtt_client_cert"
type="textarea"
maxlength="2560"
rows="10"
/>
<InputElement v-show="mqttConfigList.mqtt_tls_cert_login" <InputElement
:label="$t('mqttadmin.ClientKey')" v-show="mqttConfigList.mqtt_tls_cert_login"
v-model="mqttConfigList.mqtt_client_key" :label="$t('mqttadmin.ClientKey')"
type="textarea" maxlength="2560" rows="10"/> v-model="mqttConfigList.mqtt_client_key"
type="textarea"
maxlength="2560"
rows="10"
/>
</CardElement> </CardElement>
<CardElement :text="$t('mqttadmin.LwtParameters')" textVariant="text-bg-primary" add-space <CardElement
v-show="mqttConfigList.mqtt_enabled" :text="$t('mqttadmin.LwtParameters')"
textVariant="text-bg-primary"
add-space
v-show="mqttConfigList.mqtt_enabled"
> >
<InputElement :label="$t('mqttadmin.LwtTopic')" <InputElement
v-model="mqttConfigList.mqtt_lwt_topic" :label="$t('mqttadmin.LwtTopic')"
type="text" maxlength="32" :prefix="mqttConfigList.mqtt_topic" v-model="mqttConfigList.mqtt_lwt_topic"
:placeholder="$t('mqttadmin.LwtTopicHint')"/> type="text"
maxlength="32"
:prefix="mqttConfigList.mqtt_topic"
:placeholder="$t('mqttadmin.LwtTopicHint')"
/>
<InputElement :label="$t('mqttadmin.LwtOnline')" <InputElement
v-model="mqttConfigList.mqtt_lwt_online" :label="$t('mqttadmin.LwtOnline')"
type="text" maxlength="20" v-model="mqttConfigList.mqtt_lwt_online"
:placeholder="$t('mqttadmin.LwtOnlineHint')"/> type="text"
maxlength="20"
:placeholder="$t('mqttadmin.LwtOnlineHint')"
/>
<InputElement :label="$t('mqttadmin.LwtOffline')" <InputElement
v-model="mqttConfigList.mqtt_lwt_offline" :label="$t('mqttadmin.LwtOffline')"
type="text" maxlength="20" v-model="mqttConfigList.mqtt_lwt_offline"
:placeholder="$t('mqttadmin.LwtOfflineHint')"/> type="text"
maxlength="20"
:placeholder="$t('mqttadmin.LwtOfflineHint')"
/>
<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">
@ -117,39 +178,51 @@
</div> </div>
</CardElement> </CardElement>
<CardElement :text="$t('mqttadmin.HassParameters')" textVariant="text-bg-primary" add-space <CardElement
v-show="mqttConfigList.mqtt_enabled && mqttConfigList.mqtt_hass_enabled" :text="$t('mqttadmin.HassParameters')"
textVariant="text-bg-primary"
add-space
v-show="mqttConfigList.mqtt_enabled && mqttConfigList.mqtt_hass_enabled"
> >
<InputElement :label="$t('mqttadmin.HassPrefixTopic')" <InputElement
v-model="mqttConfigList.mqtt_hass_topic" :label="$t('mqttadmin.HassPrefixTopic')"
type="text" maxlength="32" v-model="mqttConfigList.mqtt_hass_topic"
:placeholder="$t('mqttadmin.HassPrefixTopicHint')"/> type="text"
maxlength="32"
:placeholder="$t('mqttadmin.HassPrefixTopicHint')"
/>
<InputElement :label="$t('mqttadmin.HassRetain')" <InputElement
v-model="mqttConfigList.mqtt_hass_retain" :label="$t('mqttadmin.HassRetain')"
type="checkbox"/> v-model="mqttConfigList.mqtt_hass_retain"
type="checkbox"
/>
<InputElement :label="$t('mqttadmin.HassExpire')" <InputElement
v-model="mqttConfigList.mqtt_hass_expire" :label="$t('mqttadmin.HassExpire')"
type="checkbox"/> v-model="mqttConfigList.mqtt_hass_expire"
type="checkbox"
/>
<InputElement :label="$t('mqttadmin.HassIndividual')" <InputElement
v-model="mqttConfigList.mqtt_hass_individualpanels" :label="$t('mqttadmin.HassIndividual')"
type="checkbox"/> v-model="mqttConfigList.mqtt_hass_individualpanels"
type="checkbox"
/>
</CardElement> </CardElement>
<FormFooter @reload="getMqttConfig"/> <FormFooter @reload="getMqttConfig" />
</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 { MqttConfig } from "@/types/MqttConfig"; import type { MqttConfig } from '@/types/MqttConfig';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -165,8 +238,8 @@ export default defineComponent({
return { return {
dataLoading: true, dataLoading: true,
mqttConfigList: {} as MqttConfig, mqttConfigList: {} as MqttConfig,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
qosTypeList: [ qosTypeList: [
{ key: 0, value: 'QOS0' }, { key: 0, value: 'QOS0' },
@ -181,7 +254,7 @@ export default defineComponent({
methods: { methods: {
getMqttConfig() { getMqttConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/mqtt/config", { headers: authHeader() }) fetch('/api/mqtt/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.mqttConfigList = data; this.mqttConfigList = data;
@ -192,21 +265,19 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.mqttConfigList)); formData.append('data', JSON.stringify(this.mqttConfigList));
fetch("/api/mqtt/config", { fetch('/api/mqtt/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

@ -1,5 +1,10 @@
<template> <template>
<BasePage :title="$t('mqttinfo.MqttInformation')" :isLoading="dataLoading" :show-reload="true" @reload="getMqttInfo"> <BasePage
:title="$t('mqttinfo.MqttInformation')"
:isLoading="dataLoading"
:show-reload="true"
@reload="getMqttInfo"
>
<CardElement :text="$t('mqttinfo.ConfigurationSummary')" textVariant="text-bg-primary"> <CardElement :text="$t('mqttinfo.ConfigurationSummary')" textVariant="text-bg-primary">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
@ -7,7 +12,11 @@
<tr> <tr>
<th>{{ $t('mqttinfo.Status') }}</th> <th>{{ $t('mqttinfo.Status') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_enabled" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_enabled"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -32,24 +41,42 @@
</tr> </tr>
<tr> <tr>
<th>{{ $t('mqttinfo.PublishInterval') }}</th> <th>{{ $t('mqttinfo.PublishInterval') }}</th>
<td>{{ $t('mqttinfo.Seconds', { sec: mqttDataList.mqtt_publish_interval }) }}</td> <td>
{{
$t('mqttinfo.Seconds', {
sec: mqttDataList.mqtt_publish_interval,
})
}}
</td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('mqttinfo.CleanSession') }}</th> <th>{{ $t('mqttinfo.CleanSession') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_clean_session" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_clean_session"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('mqttinfo.Retain') }}</th> <th>{{ $t('mqttinfo.Retain') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_retain" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_retain"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('mqttinfo.Tls') }}</th> <th>{{ $t('mqttinfo.Tls') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_tls" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_tls"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr v-show="mqttDataList.mqtt_tls"> <tr v-show="mqttDataList.mqtt_tls">
@ -59,7 +86,11 @@
<tr> <tr>
<th>{{ $t('mqttinfo.TlsCertLogin') }}</th> <th>{{ $t('mqttinfo.TlsCertLogin') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_tls_cert_login" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_tls_cert_login"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr v-show="mqttDataList.mqtt_tls_cert_login"> <tr v-show="mqttDataList.mqtt_tls_cert_login">
@ -78,7 +109,11 @@
<tr> <tr>
<th>{{ $t('mqttinfo.Status') }}</th> <th>{{ $t('mqttinfo.Status') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_hass_enabled" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_hass_enabled"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -88,19 +123,31 @@
<tr> <tr>
<th>{{ $t('mqttinfo.Retain') }}</th> <th>{{ $t('mqttinfo.Retain') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_hass_retain" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_hass_retain"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('mqttinfo.Expire') }}</th> <th>{{ $t('mqttinfo.Expire') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_hass_expire" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_hass_expire"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('mqttinfo.IndividualPanels') }}</th> <th>{{ $t('mqttinfo.IndividualPanels') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_hass_individualpanels" true_text="mqttinfo.Enabled" false_text="mqttinfo.Disabled" /> <StatusBadge
:status="mqttDataList.mqtt_hass_individualpanels"
true_text="mqttinfo.Enabled"
false_text="mqttinfo.Disabled"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -115,7 +162,11 @@
<tr> <tr>
<th>{{ $t('mqttinfo.ConnectionStatus') }}</th> <th>{{ $t('mqttinfo.ConnectionStatus') }}</th>
<td> <td>
<StatusBadge :status="mqttDataList.mqtt_connected" true_text="mqttinfo.Connected" false_text="mqttinfo.Disconnected" /> <StatusBadge
:status="mqttDataList.mqtt_connected"
true_text="mqttinfo.Connected"
false_text="mqttinfo.Disconnected"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -137,7 +188,7 @@ export default defineComponent({
components: { components: {
BasePage, BasePage,
CardElement, CardElement,
StatusBadge StatusBadge,
}, },
data() { data() {
return { return {
@ -151,7 +202,7 @@ export default defineComponent({
methods: { methods: {
getMqttInfo() { getMqttInfo() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/mqtt/status", { headers: authHeader() }) fetch('/api/mqtt/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.mqttDataList = data; this.mqttDataList = data;

View File

@ -6,75 +6,105 @@
<form @submit="saveNetworkConfig"> <form @submit="saveNetworkConfig">
<CardElement :text="$t('networkadmin.WifiConfiguration')" textVariant="text-bg-primary"> <CardElement :text="$t('networkadmin.WifiConfiguration')" textVariant="text-bg-primary">
<InputElement :label="$t('networkadmin.WifiSsid')" <InputElement
v-model="networkConfigList.ssid" :label="$t('networkadmin.WifiSsid')"
type="text" maxlength="32"/> v-model="networkConfigList.ssid"
type="text"
maxlength="32"
/>
<InputElement :label="$t('networkadmin.WifiPassword')" <InputElement
v-model="networkConfigList.password" :label="$t('networkadmin.WifiPassword')"
type="password" maxlength="64"/> v-model="networkConfigList.password"
type="password"
maxlength="64"
/>
<InputElement :label="$t('networkadmin.Hostname')" <InputElement
v-model="networkConfigList.hostname" :label="$t('networkadmin.Hostname')"
type="text" maxlength="32" v-model="networkConfigList.hostname"
type="text"
maxlength="32"
> >
<div class="alert alert-secondary" role="alert" v-html="$t('networkadmin.HostnameHint')"></div> <div class="alert alert-secondary" role="alert" v-html="$t('networkadmin.HostnameHint')"></div>
</InputElement> </InputElement>
<InputElement :label="$t('networkadmin.EnableDhcp')" <InputElement :label="$t('networkadmin.EnableDhcp')" v-model="networkConfigList.dhcp" type="checkbox" />
v-model="networkConfigList.dhcp"
type="checkbox"/>
</CardElement> </CardElement>
<CardElement :text="$t('networkadmin.StaticIpConfiguration')" textVariant="text-bg-primary" add-space <CardElement
v-show="!networkConfigList.dhcp" :text="$t('networkadmin.StaticIpConfiguration')"
textVariant="text-bg-primary"
add-space
v-show="!networkConfigList.dhcp"
> >
<InputElement :label="$t('networkadmin.IpAddress')" <InputElement
v-model="networkConfigList.ipaddress" :label="$t('networkadmin.IpAddress')"
type="text" maxlength="32"/> v-model="networkConfigList.ipaddress"
type="text"
maxlength="32"
/>
<InputElement :label="$t('networkadmin.Netmask')" <InputElement
v-model="networkConfigList.netmask" :label="$t('networkadmin.Netmask')"
type="text" maxlength="32"/> v-model="networkConfigList.netmask"
type="text"
maxlength="32"
/>
<InputElement :label="$t('networkadmin.DefaultGateway')" <InputElement
v-model="networkConfigList.gateway" :label="$t('networkadmin.DefaultGateway')"
type="text" maxlength="32"/> v-model="networkConfigList.gateway"
type="text"
maxlength="32"
/>
<InputElement :label="$t('networkadmin.Dns', { num: 1 })" <InputElement
v-model="networkConfigList.dns1" :label="$t('networkadmin.Dns', { num: 1 })"
type="text" maxlength="32"/> v-model="networkConfigList.dns1"
type="text"
maxlength="32"
/>
<InputElement :label="$t('networkadmin.Dns', { num: 2 })" <InputElement
v-model="networkConfigList.dns2" :label="$t('networkadmin.Dns', { num: 2 })"
type="text" maxlength="32"/> v-model="networkConfigList.dns2"
type="text"
maxlength="32"
/>
</CardElement> </CardElement>
<CardElement :text="$t('networkadmin.MdnsSettings')" textVariant="text-bg-primary" add-space> <CardElement :text="$t('networkadmin.MdnsSettings')" textVariant="text-bg-primary" add-space>
<InputElement :label="$t('networkadmin.EnableMdns')" <InputElement
v-model="networkConfigList.mdnsenabled" :label="$t('networkadmin.EnableMdns')"
type="checkbox"/> v-model="networkConfigList.mdnsenabled"
type="checkbox"
/>
</CardElement> </CardElement>
<CardElement :text="$t('networkadmin.AdminAp')" textVariant="text-bg-primary" add-space> <CardElement :text="$t('networkadmin.AdminAp')" textVariant="text-bg-primary" add-space>
<InputElement :label="$t('networkadmin.ApTimeout')" <InputElement
v-model="networkConfigList.aptimeout" :label="$t('networkadmin.ApTimeout')"
type="number" min="0" max="99999" v-model="networkConfigList.aptimeout"
:postfix="$t('networkadmin.Minutes')" type="number"
:tooltip="$t('networkadmin.ApTimeoutHint')"/> min="0"
max="99999"
:postfix="$t('networkadmin.Minutes')"
:tooltip="$t('networkadmin.ApTimeoutHint')"
/>
</CardElement> </CardElement>
<FormFooter @reload="getNetworkConfig"/> <FormFooter @reload="getNetworkConfig" />
</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 { NetworkConfig } from "@/types/NetworkConfig"; import type { NetworkConfig } from '@/types/NetworkConfig';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -90,8 +120,8 @@ export default defineComponent({
return { return {
dataLoading: true, dataLoading: true,
networkConfigList: {} as NetworkConfig, networkConfigList: {} as NetworkConfig,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
}; };
}, },
@ -101,7 +131,7 @@ export default defineComponent({
methods: { methods: {
getNetworkConfig() { getNetworkConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/network/config", { headers: authHeader() }) fetch('/api/network/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.networkConfigList = data; this.networkConfigList = data;
@ -112,21 +142,19 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.networkConfigList)); formData.append('data', JSON.stringify(this.networkConfigList));
fetch("/api/network/config", { fetch('/api/network/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

@ -1,5 +1,10 @@
<template> <template>
<BasePage :title="$t('networkinfo.NetworkInformation')" :isLoading="dataLoading" :show-reload="true" @reload="getNetworkInfo"> <BasePage
:title="$t('networkinfo.NetworkInformation')"
:isLoading="dataLoading"
:show-reload="true"
@reload="getNetworkInfo"
>
<WifiStationInfo :networkStatus="networkDataList" /> <WifiStationInfo :networkStatus="networkDataList" />
<div class="mt-5"></div> <div class="mt-5"></div>
<WifiApInfo :networkStatus="networkDataList" /> <WifiApInfo :networkStatus="networkDataList" />
@ -13,10 +18,10 @@
<script lang="ts"> <script lang="ts">
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import InterfaceApInfo from "@/components/InterfaceApInfo.vue"; import InterfaceApInfo from '@/components/InterfaceApInfo.vue';
import InterfaceNetworkInfo from "@/components/InterfaceNetworkInfo.vue"; import InterfaceNetworkInfo from '@/components/InterfaceNetworkInfo.vue';
import WifiApInfo from "@/components/WifiApInfo.vue"; import WifiApInfo from '@/components/WifiApInfo.vue';
import WifiStationInfo from "@/components/WifiStationInfo.vue"; import WifiStationInfo from '@/components/WifiStationInfo.vue';
import type { NetworkStatus } from '@/types/NetworkStatus'; import type { NetworkStatus } from '@/types/NetworkStatus';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -33,7 +38,7 @@ export default defineComponent({
return { return {
dataLoading: true, dataLoading: true,
networkDataList: {} as NetworkStatus, networkDataList: {} as NetworkStatus,
} };
}, },
created() { created() {
this.getNetworkInfo(); this.getNetworkInfo();
@ -41,7 +46,7 @@ export default defineComponent({
methods: { methods: {
getNetworkInfo() { getNetworkInfo() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/network/status", { headers: authHeader() }) fetch('/api/network/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.networkDataList = data; this.networkDataList = data;
@ -50,4 +55,4 @@ export default defineComponent({
}, },
}, },
}); });
</script> </script>

View File

@ -6,37 +6,56 @@
<form @submit="saveNtpConfig"> <form @submit="saveNtpConfig">
<CardElement :text="$t('ntpadmin.NtpConfiguration')" textVariant="text-bg-primary"> <CardElement :text="$t('ntpadmin.NtpConfiguration')" textVariant="text-bg-primary">
<InputElement :label="$t('ntpadmin.TimeServer')" <InputElement
v-model="ntpConfigList.ntp_server" :label="$t('ntpadmin.TimeServer')"
type="text" maxlength="32" v-model="ntpConfigList.ntp_server"
:tooltip="$t('ntpadmin.TimeServerHint')"/> type="text"
maxlength="32"
:tooltip="$t('ntpadmin.TimeServerHint')"
/>
<div class="row mb-3"> <div class="row mb-3">
<label for="inputTimezone" class="col-sm-2 col-form-label">{{ $t('ntpadmin.Timezone') }}</label> <label for="inputTimezone" class="col-sm-2 col-form-label">{{ $t('ntpadmin.Timezone') }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-select" v-model="timezoneSelect"> <select class="form-select" v-model="timezoneSelect">
<option v-for="(config, name) in timezoneList" :key="name + '---' + config" <option
:value="name + '---' + config"> v-for="(config, name) in timezoneList"
:key="name + '---' + config"
:value="name + '---' + config"
>
{{ name }} {{ name }}
</option> </option>
</select> </select>
</div> </div>
</div> </div>
<InputElement :label="$t('ntpadmin.TimezoneConfig')" <InputElement
v-model="ntpConfigList.ntp_timezone" :label="$t('ntpadmin.TimezoneConfig')"
type="text" maxlength="32" disabled/> v-model="ntpConfigList.ntp_timezone"
type="text"
maxlength="32"
disabled
/>
</CardElement> </CardElement>
<CardElement :text="$t('ntpadmin.LocationConfiguration')" textVariant="text-bg-primary" add-space> <CardElement :text="$t('ntpadmin.LocationConfiguration')" textVariant="text-bg-primary" add-space>
<InputElement :label="$t('ntpadmin.Latitude')" <InputElement
v-model="ntpConfigList.latitude" :label="$t('ntpadmin.Latitude')"
type="number" min="-90" max="90" step="any"/> v-model="ntpConfigList.latitude"
type="number"
<InputElement :label="$t('ntpadmin.Longitude')" min="-90"
v-model="ntpConfigList.longitude" max="90"
type="number" min="-180" max="180" step="any"/> step="any"
/>
<InputElement
:label="$t('ntpadmin.Longitude')"
v-model="ntpConfigList.longitude"
type="number"
min="-180"
max="180"
step="any"
/>
<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">
@ -52,17 +71,13 @@
</div> </div>
</div> </div>
</CardElement> </CardElement>
<FormFooter @reload="getNtpConfig"/> <FormFooter @reload="getNtpConfig" />
</form> </form>
<CardElement :text="$t('ntpadmin.ManualTimeSynchronization')" textVariant="text-bg-primary" add-space> <CardElement :text="$t('ntpadmin.ManualTimeSynchronization')" textVariant="text-bg-primary" add-space>
<InputElement :label="$t('ntpadmin.CurrentOpenDtuTime')" <InputElement :label="$t('ntpadmin.CurrentOpenDtuTime')" v-model="mcuTime" type="text" disabled />
v-model="mcuTime"
type="text" disabled/>
<InputElement :label="$t('ntpadmin.CurrentLocalTime')" <InputElement :label="$t('ntpadmin.CurrentLocalTime')" v-model="localTime" type="text" disabled />
v-model="localTime"
type="text" disabled/>
<div class="text-center mb-3"> <div class="text-center mb-3">
<button type="button" class="btn btn-danger" @click="setCurrentTime()"> <button type="button" class="btn btn-danger" @click="setCurrentTime()">
@ -76,11 +91,11 @@
<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 InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import type { NtpConfig } from "@/types/NtpConfig"; import type { NtpConfig } from '@/types/NtpConfig';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { BIconInfoCircle } from 'bootstrap-icons-vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue';
@ -100,12 +115,12 @@ export default defineComponent({
timezoneLoading: true, timezoneLoading: true,
ntpConfigList: {} as NtpConfig, ntpConfigList: {} as NtpConfig,
timezoneList: {}, timezoneList: {},
timezoneSelect: "", timezoneSelect: '',
mcuTime: new Date(), mcuTime: new Date(),
localTime: new Date(), localTime: new Date(),
dataAgeInterval: 0, dataAgeInterval: 0,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
sunsetTypeList: [ sunsetTypeList: [
{ key: 0, value: 'OFFICIAL' }, { key: 0, value: 'OFFICIAL' },
@ -117,8 +132,8 @@ export default defineComponent({
}, },
watch: { watch: {
timezoneSelect: function (newValue) { timezoneSelect: function (newValue) {
this.ntpConfigList.ntp_timezone = newValue.split("---")[1]; this.ntpConfigList.ntp_timezone = newValue.split('---')[1];
this.ntpConfigList.ntp_timezone_descr = newValue.split("---")[0]; this.ntpConfigList.ntp_timezone_descr = newValue.split('---')[0];
}, },
}, },
created() { created() {
@ -136,7 +151,7 @@ export default defineComponent({
}, },
getTimezoneList() { getTimezoneList() {
this.timezoneLoading = true; this.timezoneLoading = true;
fetch("/zones.json") fetch('/zones.json')
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
this.timezoneList = data; this.timezoneList = data;
@ -145,31 +160,23 @@ export default defineComponent({
}, },
getNtpConfig() { getNtpConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/ntp/config", { headers: authHeader() }) fetch('/api/ntp/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((data) => {
(data) => { this.ntpConfigList = data;
this.ntpConfigList = data; this.timezoneSelect =
this.timezoneSelect = this.ntpConfigList.ntp_timezone_descr + '---' + this.ntpConfigList.ntp_timezone;
this.ntpConfigList.ntp_timezone_descr + this.dataLoading = false;
"---" + });
this.ntpConfigList.ntp_timezone;
this.dataLoading = false;
}
);
}, },
getCurrentTime() { getCurrentTime() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/ntp/time", { headers: authHeader() }) fetch('/api/ntp/time', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((data) => {
(data) => { this.mcuTime = new Date(data.year, data.month - 1, data.day, data.hour, data.minute, data.second);
this.mcuTime = new Date( this.dataLoading = false;
data.year, data.month - 1, data.day, });
data.hour, data.minute, data.second);
this.dataLoading = false;
}
);
}, },
setCurrentTime() { setCurrentTime() {
const formData = new FormData(); const formData = new FormData();
@ -182,21 +189,19 @@ export default defineComponent({
second: this.localTime.getSeconds(), second: this.localTime.getSeconds(),
}; };
console.log(time); console.log(time);
formData.append("data", JSON.stringify(time)); formData.append('data', JSON.stringify(time));
fetch("/api/ntp/time", { fetch('/api/ntp/time', {
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; })
}
)
.then(() => { .then(() => {
this.getCurrentTime(); this.getCurrentTime();
}); });
@ -205,22 +210,20 @@ export default defineComponent({
e.preventDefault(); e.preventDefault();
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.ntpConfigList)); formData.append('data', JSON.stringify(this.ntpConfigList));
fetch("/api/ntp/config", { fetch('/api/ntp/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

@ -28,7 +28,11 @@
<tr> <tr>
<th>{{ $t('ntpinfo.Status') }}</th> <th>{{ $t('ntpinfo.Status') }}</th>
<td> <td>
<StatusBadge :status="ntpDataList.ntp_status" true_text="ntpinfo.Synced" false_text="ntpinfo.NotSynced" /> <StatusBadge
:status="ntpDataList.ntp_status"
true_text="ntpinfo.Synced"
false_text="ntpinfo.NotSynced"
/>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -38,20 +42,28 @@
<tr> <tr>
<th>{{ $t('ntpinfo.Sunrise') }}</th> <th>{{ $t('ntpinfo.Sunrise') }}</th>
<td v-if="ntpDataList.sun_isSunsetAvailable">{{ ntpDataList.sun_risetime }}</td> <td v-if="ntpDataList.sun_isSunsetAvailable">
{{ ntpDataList.sun_risetime }}
</td>
<td v-else>{{ $t('ntpinfo.NotAvailable') }}</td> <td v-else>{{ $t('ntpinfo.NotAvailable') }}</td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('ntpinfo.Sunset') }}</th> <th>{{ $t('ntpinfo.Sunset') }}</th>
<td v-if="ntpDataList.sun_isSunsetAvailable">{{ ntpDataList.sun_settime }}</td> <td v-if="ntpDataList.sun_isSunsetAvailable">
{{ ntpDataList.sun_settime }}
</td>
<td v-else>{{ $t('ntpinfo.NotAvailable') }}</td> <td v-else>{{ $t('ntpinfo.NotAvailable') }}</td>
</tr> </tr>
<tr> <tr>
<th>{{ $t('ntpinfo.Mode') }}</th> <th>{{ $t('ntpinfo.Mode') }}</th>
<td> <td>
<StatusBadge :status="ntpDataList.sun_isDayPeriod" <StatusBadge
true_text="ntpinfo.Day" true_class="text-bg-warning" :status="ntpDataList.sun_isDayPeriod"
false_text="ntpinfo.Night" false_class="text-bg-dark" /> true_text="ntpinfo.Day"
true_class="text-bg-warning"
false_text="ntpinfo.Night"
false_class="text-bg-dark"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -65,7 +77,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 { NtpStatus } from "@/types/NtpStatus"; import type { NtpStatus } from '@/types/NtpStatus';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -87,7 +99,7 @@ export default defineComponent({
methods: { methods: {
getNtpInfo() { getNtpInfo() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/ntp/status", { headers: authHeader() }) fetch('/api/ntp/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.ntpDataList = data; this.ntpDataList = data;
@ -96,4 +108,4 @@ export default defineComponent({
}, },
}, },
}); });
</script> </script>

View File

@ -6,31 +6,40 @@
<form @submit="savePasswordConfig"> <form @submit="savePasswordConfig">
<CardElement :text="$t('securityadmin.AdminPassword')" textVariant="text-bg-primary"> <CardElement :text="$t('securityadmin.AdminPassword')" textVariant="text-bg-primary">
<InputElement :label="$t('securityadmin.Password')" <InputElement
v-model="securityConfigList.password" :label="$t('securityadmin.Password')"
type="password" maxlength="64"/> v-model="securityConfigList.password"
type="password"
maxlength="64"
/>
<InputElement :label="$t('securityadmin.RepeatPassword')" <InputElement
v-model="passwordRepeat" :label="$t('securityadmin.RepeatPassword')"
type="password" maxlength="64"/> v-model="passwordRepeat"
type="password"
maxlength="64"
/>
<div class="alert alert-secondary" role="alert" v-html="$t('securityadmin.PasswordHint')"></div> <div class="alert alert-secondary" role="alert" v-html="$t('securityadmin.PasswordHint')"></div>
</CardElement> </CardElement>
<CardElement :text="$t('securityadmin.Permissions')" textVariant="text-bg-primary" add-space> <CardElement :text="$t('securityadmin.Permissions')" textVariant="text-bg-primary" add-space>
<InputElement :label="$t('securityadmin.ReadOnly')" <InputElement
v-model="securityConfigList.allow_readonly" :label="$t('securityadmin.ReadOnly')"
type="checkbox" wide/> v-model="securityConfigList.allow_readonly"
type="checkbox"
wide
/>
</CardElement> </CardElement>
<FormFooter @reload="getPasswordConfig"/> <FormFooter @reload="getPasswordConfig" />
</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';
@ -49,12 +58,12 @@ export default defineComponent({
data() { data() {
return { return {
dataLoading: true, dataLoading: true,
alertMessage: "", alertMessage: '',
alertType: "info", alertType: 'info',
showAlert: false, showAlert: false,
securityConfigList: {} as SecurityConfig, securityConfigList: {} as SecurityConfig,
passwordRepeat: "", passwordRepeat: '',
}; };
}, },
created() { created() {
@ -63,43 +72,39 @@ export default defineComponent({
methods: { methods: {
getPasswordConfig() { getPasswordConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/security/config", { headers: authHeader() }) fetch('/api/security/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then((data) => {
(data) => { this.securityConfigList = data;
this.securityConfigList = data; this.passwordRepeat = this.securityConfigList.password;
this.passwordRepeat = this.securityConfigList.password; this.dataLoading = false;
this.dataLoading = false; });
}
);
}, },
savePasswordConfig(e: Event) { savePasswordConfig(e: Event) {
e.preventDefault(); e.preventDefault();
if (this.securityConfigList.password != this.passwordRepeat) { if (this.securityConfigList.password != this.passwordRepeat) {
this.alertMessage = "Passwords are not equal"; this.alertMessage = 'Passwords are not equal';
this.alertType = "warning"; this.alertType = 'warning';
this.showAlert = true; this.showAlert = true;
return; return;
} }
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(this.securityConfigList)); formData.append('data', JSON.stringify(this.securityConfigList));
fetch("/api/security/config", { fetch('/api/security/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

@ -15,11 +15,11 @@
<script lang="ts"> <script lang="ts">
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import FirmwareInfo from "@/components/FirmwareInfo.vue"; import FirmwareInfo from '@/components/FirmwareInfo.vue';
import HardwareInfo from "@/components/HardwareInfo.vue"; import HardwareInfo from '@/components/HardwareInfo.vue';
import MemoryInfo from "@/components/MemoryInfo.vue"; import MemoryInfo from '@/components/MemoryInfo.vue';
import HeapDetails from "@/components/HeapDetails.vue"; import HeapDetails from '@/components/HeapDetails.vue';
import RadioInfo from "@/components/RadioInfo.vue"; import RadioInfo from '@/components/RadioInfo.vue';
import type { SystemStatus } from '@/types/SystemStatus'; import type { SystemStatus } from '@/types/SystemStatus';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -38,16 +38,16 @@ export default defineComponent({
dataLoading: true, dataLoading: true,
systemDataList: {} as SystemStatus, systemDataList: {} as SystemStatus,
allowVersionInfo: false, allowVersionInfo: false,
} };
}, },
created() { created() {
this.allowVersionInfo = (localStorage.getItem("allowVersionInfo") || "0") == "1"; this.allowVersionInfo = (localStorage.getItem('allowVersionInfo') || '0') == '1';
this.getSystemInfo(); this.getSystemInfo();
}, },
methods: { methods: {
getSystemInfo() { getSystemInfo() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/system/status", { headers: authHeader() }) fetch('/api/system/status', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.systemDataList = data; this.systemDataList = data;
@ -55,7 +55,7 @@ export default defineComponent({
if (this.allowVersionInfo) { if (this.allowVersionInfo) {
this.getUpdateInfo(); this.getUpdateInfo();
} }
}) });
}, },
getUpdateInfo() { getUpdateInfo() {
if (this.systemDataList.git_hash === undefined) { if (this.systemDataList.git_hash === undefined) {
@ -64,47 +64,51 @@ export default defineComponent({
// If the left char is a "g" the value is the git hash (remove the "g") // If the left char is a "g" the value is the git hash (remove the "g")
this.systemDataList.git_is_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g'; this.systemDataList.git_is_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g';
this.systemDataList.git_hash = this.systemDataList.git_is_hash ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash; this.systemDataList.git_hash = this.systemDataList.git_is_hash
? this.systemDataList.git_hash?.substring(1)
: this.systemDataList.git_hash;
// Handle format "v0.1-5-gabcdefh" // Handle format "v0.1-5-gabcdefh"
if (this.systemDataList.git_hash?.lastIndexOf("-") >= 0) { if (this.systemDataList.git_hash?.lastIndexOf('-') >= 0) {
this.systemDataList.git_hash = this.systemDataList.git_hash.substring(this.systemDataList.git_hash.lastIndexOf("-") + 2) this.systemDataList.git_hash = this.systemDataList.git_hash.substring(
this.systemDataList.git_hash.lastIndexOf('-') + 2
);
this.systemDataList.git_is_hash = true; this.systemDataList.git_is_hash = true;
} }
const fetchUrl = "https://api.github.com/repos/tbnobody/OpenDTU/compare/" const fetchUrl =
+ this.systemDataList.git_hash + "...HEAD"; 'https://api.github.com/repos/tbnobody/OpenDTU/compare/' + this.systemDataList.git_hash + '...HEAD';
fetch(fetchUrl) fetch(fetchUrl)
.then((response) => { .then((response) => {
if (response.ok) { if (response.ok) {
return response.json() return response.json();
} }
throw new Error(this.$t("systeminfo.VersionError")); throw new Error(this.$t('systeminfo.VersionError'));
}) })
.then((data) => { .then((data) => {
if (data.total_commits > 0) { if (data.total_commits > 0) {
this.systemDataList.update_text = this.$t("systeminfo.VersionNew"); this.systemDataList.update_text = this.$t('systeminfo.VersionNew');
this.systemDataList.update_status = "text-bg-danger"; this.systemDataList.update_status = 'text-bg-danger';
this.systemDataList.update_url = data.html_url; this.systemDataList.update_url = data.html_url;
} else { } else {
this.systemDataList.update_text = this.$t("systeminfo.VersionOk"); this.systemDataList.update_text = this.$t('systeminfo.VersionOk');
this.systemDataList.update_status = "text-bg-success"; this.systemDataList.update_status = 'text-bg-success';
} }
}) })
.catch((error: Error) => { .catch((error: Error) => {
this.systemDataList.update_text = error.message; this.systemDataList.update_text = error.message;
this.systemDataList.update_status = "text-bg-secondary"; this.systemDataList.update_status = 'text-bg-secondary';
}); });
} },
}, },
watch: { watch: {
allowVersionInfo(allow: boolean) { allowVersionInfo(allow: boolean) {
localStorage.setItem("allowVersionInfo", allow ? "1" : "0"); localStorage.setItem('allowVersionInfo', allow ? '1' : '0');
if (allow) { if (allow) {
this.getUpdateInfo(); this.getUpdateInfo();
} }
} },
} },
}); });
</script> </script>