Merge remote-tracking branch 'tbnobody/OpenDTU/master'
This commit is contained in:
commit
905dc359a5
@ -163,6 +163,7 @@ This can be achieved by editing the 'platformio.ini' file and add/change one or
|
||||
* clean the sources: `platformio run -e generic -t clean`
|
||||
* erase flash: `platformio run -e generic -t erase`
|
||||
### using the pre-compiled .bin files
|
||||
The pre-compiled files can be found on the [github page](https://github.com/tbnobody/OpenDTU) in the tab "Actions" and the sub menu "OpenDTU Build". Just choose the latest build from the master branch (blue font). You need to be logged in with your github account to download the files.
|
||||
Use a ESP32 flash tool of your choice and flash the .bin files to the right addresses:
|
||||
|
||||
| Address | File |
|
||||
@ -172,7 +173,8 @@ Use a ESP32 flash tool of your choice and flash the .bin files to the right addr
|
||||
| 0xe000 | boot_app0.bin |
|
||||
| 0x10000 | opendtu-*.bin |
|
||||
|
||||
Make sure too uncheck the DoNotChgBin option. Otherwise you will maybe get errors like "invalid header"
|
||||
Make sure too uncheck the DoNotChgBin option. Otherwise you will maybe get errors like "invalid header".
|
||||
For further upgraded you can just use the web interface and upload the opendtu-*.bin file.
|
||||
|
||||
## First configuration
|
||||
* After the initial flashing of the microcontroller, an Access Point called "OpenDTU-*" is opened. The default password is "openDTU42".
|
||||
|
||||
@ -39,7 +39,7 @@ serial will be replaced with the serial number of the inverter.
|
||||
| [serial]/0/temperature | R | Temperature of inverter in degree celsius | Degree Celsius (°C) |
|
||||
| [serial]/0/voltage | R | AC voltage in volt | Volt (V) |
|
||||
| [serial]/0/yieldday | R | Energy converted to AC per day in watt hours | Watt hours (Wh) |
|
||||
| [serial]/0/yieldtotal | R | Energy converted to AC since reset watt hours | Watt hours (Wh) |
|
||||
| [serial]/0/yieldtotal | R | Energy converted to AC since reset watt hours | Watt hours (kWh) |
|
||||
|
||||
### DC input channel topics
|
||||
|
||||
@ -52,7 +52,7 @@ serial will be replaced with the serial number of the inverter.
|
||||
| [serial]/[1-4]/power | R | DC power of specific input in watt | Watt (W) |
|
||||
| [serial]/[1-4]/voltage | R | DC voltage of specific input in volt | Volt (V) |
|
||||
| [serial]/[1-4]/yieldday | R | Energy converted to AC per day on specific input | Watt hours (Wh) |
|
||||
| [serial]/[1-4]/yieldtotal | R | Energy converted to AC since reset on specific input | Watt hours (Wh) |
|
||||
| [serial]/[1-4]/yieldtotal | R | Energy converted to AC since reset on specific input | Watt hours (kWh) |
|
||||
|
||||
### Inverter limit specific topics
|
||||
|
||||
@ -69,4 +69,4 @@ cmd topics are used to set values. Status topics are updated from values set in
|
||||
| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |
|
||||
| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) |
|
||||
| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 |
|
||||
| [serial]/cmd/restart | W | Restarts the inverters (also resets YieldDay) | 1 |
|
||||
| [serial]/cmd/restart | W | Restarts the inverters (also resets YieldDay) | 1 |
|
||||
|
||||
14
webapp/.eslintrc.cjs
Normal file
14
webapp/.eslintrc.cjs
Normal file
@ -0,0 +1,14 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
27
webapp/.gitignore
vendored
27
webapp/.gitignore
vendored
@ -1,21 +1,26 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
|
||||
3
webapp/.vscode/extensions.json
vendored
Normal file
3
webapp/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
@ -1,24 +1,25 @@
|
||||
# opendtu
|
||||
|
||||
## Project setup
|
||||
```
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn serve
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
```
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
```sh
|
||||
yarn lint
|
||||
```
|
||||
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
1
webapp/env.d.ts
vendored
Normal file
1
webapp/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
16
webapp/index.html
Normal file
16
webapp/index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenDTU</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but OpenDTU doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -3,57 +3,36 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "^5.2.2",
|
||||
"bootstrap-icons-vue": "^1.8.1",
|
||||
"core-js": "^3.25.5",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue": "^3.2.40",
|
||||
"vue-class-component": "^8.0.0-0",
|
||||
"vue": "^3.2.41",
|
||||
"vue-router": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@types/bootstrap": "^5.2.5",
|
||||
"@types/node": "^18.8.3",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||
"@vue/cli-plugin-router": "^5.0.6",
|
||||
"@vue/cli-plugin-typescript": "^5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vitejs/plugin-vue": "^3.1.2",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-plugin-vue": "^9.6.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^4.8.4",
|
||||
"vue-cli-plugin-compression": "~2.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
]
|
||||
"vite": "^3.1.8",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-css-injected-by-js": "^2.1.0",
|
||||
"vue-tsc": "^1.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@ -5,7 +5,7 @@
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import NavBar from "./components/NavBar.vue";
|
||||
|
||||
export default {
|
||||
|
||||
30
webapp/src/components/BasePage.vue
Normal file
30
webapp/src/components/BasePage.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div :class="{'container-xxl': !isWideScreen,
|
||||
'container-fluid': isWideScreen}" role="main">
|
||||
<div class="page-header">
|
||||
<h1>{{ title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-center" v-if="isLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!isLoading">
|
||||
<slot />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
isLoading: { type: Boolean, required: false, default: false },
|
||||
isWideScreen: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -6,11 +6,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeUnmount, ref, watch } from "vue";
|
||||
import Alert from "bootstrap/js/dist/alert";
|
||||
|
||||
export const toInteger = (value, defaultValue = NaN) => {
|
||||
export const toInteger = (value: number, defaultValue = NaN) => {
|
||||
return Number.isInteger(value) ? value : defaultValue;
|
||||
};
|
||||
|
||||
@ -26,8 +26,8 @@ export default defineComponent({
|
||||
},
|
||||
emits: ["dismissed", "dismiss-count-down", "update:modelValue"],
|
||||
setup(props, { emit }) {
|
||||
const element = undefined;
|
||||
let instance = undefined;
|
||||
const element = ref<HTMLElement>();
|
||||
const instance = ref<Alert>();
|
||||
const classes = computed(() => ({
|
||||
[`alert-${props.variant}`]: props.variant,
|
||||
show: props.modelValue,
|
||||
@ -35,9 +35,9 @@ export default defineComponent({
|
||||
fade: props.modelValue,
|
||||
}));
|
||||
|
||||
let _countDownTimeout = 0;
|
||||
let _countDownTimeout: number | undefined = 0;
|
||||
|
||||
const parseCountDown = (value) => {
|
||||
const parseCountDown = (value: boolean | number) => {
|
||||
if (typeof value === "boolean") {
|
||||
return 0;
|
||||
}
|
||||
@ -57,8 +57,8 @@ export default defineComponent({
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearCountDownInterval();
|
||||
instance?.dispose();
|
||||
instance = undefined;
|
||||
instance.value?.dispose();
|
||||
instance.value = undefined;
|
||||
});
|
||||
|
||||
const parsedModelValue = computed(() => {
|
||||
@ -76,8 +76,8 @@ export default defineComponent({
|
||||
|
||||
const handleShowAndModelChanged = () => {
|
||||
countDown.value = parseCountDown(props.modelValue);
|
||||
if ((parsedModelValue.value || props.show) && !instance)
|
||||
instance = new Alert(element);
|
||||
if ((parsedModelValue.value || props.show) && !instance.value)
|
||||
instance.value = new Alert(element.value as HTMLElement);
|
||||
};
|
||||
|
||||
const dismissClicked = () => {
|
||||
@ -1,227 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Config Management</h1>
|
||||
</div>
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="text-center" v-if="loading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!loading">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Backup: Configuration File Backup</div>
|
||||
<div class="card-body text-center">
|
||||
Backup the configuration file
|
||||
<button class="btn btn-primary" @click="downloadConfig">Backup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Restore: Restore the Configuration File</div>
|
||||
<div class="card-body text-center">
|
||||
|
||||
<div v-if="!uploading && UploadError != ''">
|
||||
<p class="h1 mb-2">
|
||||
<BIconExclamationCircleFill />
|
||||
</p>
|
||||
<span style="vertical-align: middle" class="ml-2">
|
||||
{{ UploadError }}
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-light" @click="clear">
|
||||
<BIconArrowLeft /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading && UploadSuccess">
|
||||
<span class="h1 mb-2">
|
||||
<BIconCheckCircle />
|
||||
</span>
|
||||
<span> Upload Success </span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-primary" @click="clear">
|
||||
<BIconArrowLeft /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading">
|
||||
<div class="form-group pt-2 mt-3">
|
||||
<input class="form-control" type="file" ref="file" accept=".json" @change="uploadConfig" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="uploading">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" :style="{ width: progress + '%' }"
|
||||
v-bind:aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100">
|
||||
{{ progress }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
<b>Note:</b> This operation replaces the configuration file with the restored configuration and
|
||||
restarts OpenDTU to apply all settings.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Initialize: Perform Factory Reset</div>
|
||||
<div class="card-body text-center">
|
||||
|
||||
<button class="btn btn-danger" @click="onFactoryResetModal">Restore Factory-Default Settings
|
||||
</button>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
<b>Note:</b> Click Restore Factory-Default Settings to restore and initialize the
|
||||
factory-default settings and reboot.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="modal" id="factoryReset" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Factory Reset</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete the current configuration and reset all settings to their
|
||||
factory defaults?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onFactoryResetCancel"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" @click="onFactoryResetPerform">Factory
|
||||
Reset!</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
BIconExclamationCircleFill,
|
||||
BIconArrowLeft,
|
||||
BIconCheckCircle
|
||||
} from 'bootstrap-icons-vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BIconExclamationCircleFill,
|
||||
BIconArrowLeft,
|
||||
BIconCheckCircle,
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalFactoryReset: {} as bootstrap.Modal,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
loading: true,
|
||||
uploading: false,
|
||||
progress: 0,
|
||||
UploadError: "",
|
||||
UploadSuccess: false,
|
||||
file: {} as Blob,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.modalFactoryReset = new bootstrap.Modal('#factoryReset');
|
||||
this.loading = false;
|
||||
},
|
||||
methods: {
|
||||
onFactoryResetModal() {
|
||||
this.modalFactoryReset.show();
|
||||
},
|
||||
onFactoryResetCancel() {
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
onFactoryResetPerform() {
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify({ delete: true }));
|
||||
|
||||
fetch("/api/config/delete", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
)
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
downloadConfig() {
|
||||
const link = document.createElement('a')
|
||||
link.href = "/api/config/get"
|
||||
link.download = 'config.json'
|
||||
link.click()
|
||||
},
|
||||
uploadConfig(event: Event | null) {
|
||||
this.uploading = true;
|
||||
const formData = new FormData();
|
||||
if (event !== null) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.files !== null) {
|
||||
this.file = target.files[0];
|
||||
}
|
||||
}
|
||||
const request = new XMLHttpRequest();
|
||||
request.addEventListener("load", () => {
|
||||
// request.response will hold the response from the server
|
||||
if (request.status === 200) {
|
||||
this.UploadSuccess = true;
|
||||
} else if (request.status !== 500) {
|
||||
this.UploadError = `[HTTP ERROR] ${request.statusText}`;
|
||||
} else {
|
||||
this.UploadError = request.responseText;
|
||||
}
|
||||
this.uploading = false;
|
||||
this.progress = 0;
|
||||
});
|
||||
// Upload progress
|
||||
request.upload.addEventListener("progress", (e) => {
|
||||
this.progress = Math.trunc((e.loaded / e.total) * 100);
|
||||
});
|
||||
request.withCredentials = true;
|
||||
|
||||
formData.append("config", this.file, "config");
|
||||
request.open("post", "/api/config/upload");
|
||||
request.send(formData);
|
||||
},
|
||||
clear() {
|
||||
this.UploadError = "";
|
||||
this.UploadSuccess = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -38,19 +38,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import { BIconInfoSquare } from 'bootstrap-icons-vue';
|
||||
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
|
||||
|
||||
declare interface DevInfoData {
|
||||
valid_data: boolean,
|
||||
fw_bootloader_version: number,
|
||||
fw_build_version: number,
|
||||
fw_build_datetime: Date,
|
||||
hw_part_number: number,
|
||||
hw_version: number,
|
||||
hw_model_name: string,
|
||||
}
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import type { DevInfoStatus } from "@/types/DevInfoStatus";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -58,7 +49,7 @@ export default defineComponent({
|
||||
BootstrapAlert,
|
||||
},
|
||||
props: {
|
||||
devInfoList: { type: Object as () => DevInfoData, required: true },
|
||||
devInfoList: { type: Object as PropType<DevInfoStatus>, required: true },
|
||||
},
|
||||
computed: {
|
||||
formatVersion() {
|
||||
@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>DTU Settings</h1>
|
||||
</div>
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<form @submit="saveDtuConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">DTU Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputDtuSerial" class="col-sm-2 col-form-label">Serial:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="inputDtuSerial" min="1" max="99999999999"
|
||||
placeholder="DTU Serial" v-model="dtuConfigList.dtu_serial" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPollInterval" class="col-sm-2 col-form-label">Poll Interval:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputPollInterval" min="1" max="86400"
|
||||
placeholder="Poll Interval in Seconds" v-model="dtuConfigList.dtu_pollinterval"
|
||||
aria-describedby="pollIntervalDescription" />
|
||||
<span class="input-group-text" id="pollIntervalDescription">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTimezone" class="col-sm-2 col-form-label">PA Level:</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="dtuConfigList.dtu_palevel">
|
||||
<option v-for="palevel in palevelList" :key="palevel.key" :value="palevel.key">
|
||||
{{ palevel.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
dtuConfigList: {
|
||||
dtu_serial: 0,
|
||||
dtu_pollinterval: 0,
|
||||
dtu_palevel: 0
|
||||
},
|
||||
palevelList: [
|
||||
{ key: 0, value: "Minimum (-18 dBm)" },
|
||||
{ key: 1, value: "Low (-12 dBm)" },
|
||||
{ key: 2, value: "High (-6 dBm)" },
|
||||
{ key: 3, value: "Maximum (0 dBm)" },
|
||||
],
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getDtuConfig();
|
||||
},
|
||||
methods: {
|
||||
getDtuConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/dtu/config")
|
||||
.then((response) => response.json())
|
||||
.then(
|
||||
(data) => {
|
||||
this.dtuConfigList = data;
|
||||
this.dataLoading = false;
|
||||
}
|
||||
);
|
||||
},
|
||||
saveDtuConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.dtuConfigList));
|
||||
|
||||
fetch("/api/dtu/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
39
webapp/src/components/EventLog.vue
Normal file
39
webapp/src/components/EventLog.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th scope="col">Start</th>
|
||||
<th scope="col">Stop</th>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Message</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="event in eventLogList.count" :key="event">
|
||||
<tr>
|
||||
<td>{{ timeInHours(eventLogList.events[event - 1].start_time) }}</td>
|
||||
<td>{{ timeInHours(eventLogList.events[event - 1].end_time) }}</td>
|
||||
<td>{{ eventLogList.events[event - 1].message_id }}</td>
|
||||
<td>{{ eventLogList.events[event - 1].message }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import { timestampToString } from '@/utils';
|
||||
import type { EventlogItems } from '@/types/EventlogStatus';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
eventLogList: { type: Object as PropType<EventlogItems>, required: true },
|
||||
},
|
||||
computed: {
|
||||
timeInHours() {
|
||||
return (value: number) => {
|
||||
return timestampToString(value);
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
73
webapp/src/components/FirmwareInfo.vue
Normal file
73
webapp/src/components/FirmwareInfo.vue
Normal file
@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">
|
||||
Firmware Information
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td>{{ systemStatus.hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SDK Version</th>
|
||||
<td>{{ systemStatus.sdkversion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Config Version</th>
|
||||
<td>{{ systemStatus.config_version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Firmware Version / Git Hash</th>
|
||||
<td><a :href="'https://github.com/tbnobody/OpenDTU/commits/' + systemStatus.git_hash?.substring(1)"
|
||||
target="_blank">{{ systemStatus.git_hash?.substring(1) }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Firmware Update</th>
|
||||
<td><a :href="systemStatus.update_url" target="_blank"><span class="badge"
|
||||
:class="systemStatus.update_status">{{
|
||||
systemStatus.update_text }}</span></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Reset Reason CPU 0</th>
|
||||
<td>{{ systemStatus.resetreason_0 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Reset Reason CPU 1</th>
|
||||
<td>{{ systemStatus.resetreason_1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Config save count</th>
|
||||
<td>{{ systemStatus.cfgsavecount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Uptime</th>
|
||||
<td>{{ timeInHours(systemStatus.uptime) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import type { SystemStatus } from '@/types/SystemStatus';
|
||||
import { timestampToString } from '@/utils';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
systemStatus: { type: Object as PropType<SystemStatus>, required: true },
|
||||
},
|
||||
computed: {
|
||||
timeInHours() {
|
||||
return (value: number) => {
|
||||
return timestampToString(value, true);
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -9,19 +9,19 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Chip Model</th>
|
||||
<td>{{ chipmodel }}</td>
|
||||
<td>{{ systemStatus.chipmodel }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Chip Revision</th>
|
||||
<td>{{ chiprevision }}</td>
|
||||
<td>{{ systemStatus.chiprevision }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Chip Cores</th>
|
||||
<td>{{ chipcores }}</td>
|
||||
<td>{{ systemStatus.chipcores }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>CPU Frequency</th>
|
||||
<td>{{ cpufreq }} MHz</td>
|
||||
<td>{{ systemStatus.cpufreq }} MHz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -31,14 +31,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { SystemStatus } from '@/types/SystemStatus';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
chipmodel: String,
|
||||
chiprevision: { type: Number, required: true },
|
||||
chipcores: { type: Number, required: true },
|
||||
cpufreq: { type: Number, required: true },
|
||||
systemStatus: { type: Object as PropType<SystemStatus>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,676 +0,0 @@
|
||||
<template>
|
||||
<div class="container-fluid" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Live Data</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="row gy-3">
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<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"
|
||||
: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">
|
||||
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
|
||||
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
|
||||
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
|
||||
{{ inverter.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content col-sm-9 col-md-10" id="v-pills-tabContent">
|
||||
<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-header text-white bg-primary d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'bg-danger': !inverter.reachable,
|
||||
'bg-warning': inverter.reachable && !inverter.producing,
|
||||
'bg-primary': inverter.reachable && inverter.producing,
|
||||
}">
|
||||
<div class="p-2 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
{{ inverter.name }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
Serial Number: {{ inverter.serial }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
Current Limit: <template v-if="inverter.limit_absolute > -1"> {{
|
||||
inverter.limit_absolute.toFixed(0) }}W | </template>{{
|
||||
inverter.limit_relative.toFixed(0)
|
||||
}}%
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
Data Age: {{ inverter.data_age }} seconds
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
@click="onShowLimitSettings(inverter.serial)"
|
||||
title="Show / Set Inverter Limit">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
@click="onShowPowerSettings(inverter.serial)" title="Turn Inverter on/off">
|
||||
<BIconPower style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-info"
|
||||
@click="onShowDevInfo(inverter.serial)" title="Show Inverter Info">
|
||||
<BIconCpu style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="inverter.events >= 0" type="button"
|
||||
class="btn btn-sm btn-secondary position-relative"
|
||||
@click="onShowEventlog(inverter.serial)" title="Show Eventlog">
|
||||
<BIconJournalText style="font-size:24px;" />
|
||||
<span
|
||||
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
||||
{{ inverter.events }}
|
||||
<span class="visually-hidden">unread messages</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row flex-row-reverse flex-wrap-reverse align-items-end g-3">
|
||||
<template v-for="channel in 5" :key="channel">
|
||||
<div v-if="inverter[channel - 1]" :class="`col order-${5 - channel}`">
|
||||
<InverterChannelInfo :channelData="inverter[channel - 1]"
|
||||
:channelNumber="channel - 1" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<VedirectView />
|
||||
|
||||
<div class="modal" id="eventView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Event Log</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center" v-if="eventLogLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EventLog v-if="!eventLogLoading" :eventLogList="eventLogList" />
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onHideEventlog"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="devInfoView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Inverter Info</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center" v-if="devInfoLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DevInfo v-if="!devInfoLoading" :devInfoList="devInfoList" />
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onHideDevInfo"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="limitSettingView" ref="limitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Limit Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<BootstrapAlert v-model="showAlertLimit" :variant="alertTypeLimit">
|
||||
{{ alertMessageLimit }}
|
||||
</BootstrapAlert>
|
||||
<div class="text-center" v-if="limitSettingLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!limitSettingLoading">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">Current
|
||||
Limit:</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputCurrentLimit"
|
||||
aria-describedby="currentLimitType" v-model="currentLimit" disabled />
|
||||
<span class="input-group-text" id="currentLimitType">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4" v-if="maxPower > 0">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputCurrentLimitAbsolute"
|
||||
aria-describedby="currentLimitTypeAbsolute"
|
||||
v-model="currentLimitAbsolute" disabled />
|
||||
<span class="input-group-text" id="currentLimitTypeAbsolute">W</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputLastLimitSet" class="col-sm-3 col-form-label">Last Limit Set
|
||||
Status:</label>
|
||||
<div class="col-sm-9">
|
||||
<span class="badge" :class="{
|
||||
'bg-danger': successCommandLimit == 'Failure',
|
||||
'bg-warning': successCommandLimit == 'Pending',
|
||||
'bg-success': successCommandLimit == 'Ok',
|
||||
'bg-secondary': successCommandLimit == 'Unknown',
|
||||
}">
|
||||
{{ successCommandLimit }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTargetLimit" class="col-sm-3 col-form-label">Set Limit:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input type="number" name="inputTargetLimit" class="form-control"
|
||||
id="inputTargetLimit" :min="targetLimitMin" :max="targetLimitMax"
|
||||
v-model="targetLimit">
|
||||
<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">
|
||||
<li><a class="dropdown-item" @click="onSelectType(1)" href="#">Relative
|
||||
(%)</a></li>
|
||||
<li><a class="dropdown-item" @click="onSelectType(0)" href="#">Absolute
|
||||
(W)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="targetLimitType == 0" class="alert alert-secondary mt-3"
|
||||
role="alert">
|
||||
<b>Hint:</b> If you set the limit as absolute value the display of the
|
||||
current value will only be updated after ~4 minutes.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">Set Limit
|
||||
Persistent</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">Set Limit
|
||||
Non-Persistent</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="powerSettingView" ref="powerSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Power Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<BootstrapAlert v-model="showAlertPower" :variant="alertTypePower">
|
||||
{{ alertMessagePower }}
|
||||
</BootstrapAlert>
|
||||
<div class="text-center" v-if="powerSettingLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!powerSettingLoading">
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputLastPowerSet" class="col col-form-label">Last Power Set
|
||||
Status:</label>
|
||||
<div class="col">
|
||||
<span class="badge" :class="{
|
||||
'bg-danger': successCommandPower == 'Failure',
|
||||
'bg-warning': successCommandPower == 'Pending',
|
||||
'bg-success': successCommandPower == 'Ok',
|
||||
'bg-secondary': successCommandPower == 'Unknown',
|
||||
}">
|
||||
{{ successCommandPower }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 col-6 mx-auto">
|
||||
<button type="button" class="btn btn-success" @click="onSetPowerSettings(true)">
|
||||
<BIconToggleOn class="fs-4" /> Turn On
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" @click="onSetPowerSettings(false)">
|
||||
<BIconToggleOff class="fs-4" /> Turn Off
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" @click="onSetPowerSettings(true, true)">
|
||||
<BIconArrowCounterclockwise class="fs-4" /> Restart
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import {
|
||||
BIconXCircleFill,
|
||||
BIconExclamationCircleFill,
|
||||
BIconCheckCircleFill,
|
||||
BIconSpeedometer,
|
||||
BIconPower,
|
||||
BIconCpu,
|
||||
BIconJournalText,
|
||||
BIconToggleOn,
|
||||
BIconToggleOff,
|
||||
BIconArrowCounterclockwise
|
||||
} from 'bootstrap-icons-vue';
|
||||
import EventLog from '@/components/partials/EventLog.vue';
|
||||
import DevInfo from '@/components/partials/DevInfo.vue';
|
||||
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
|
||||
import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
|
||||
import VedirectView from '@/components/partials/VedirectView.vue';
|
||||
|
||||
declare interface Inverter {
|
||||
serial: number,
|
||||
name: string,
|
||||
reachable: boolean,
|
||||
producing: boolean,
|
||||
limit_relative: 0,
|
||||
limit_absolute: 0,
|
||||
data_age: 0,
|
||||
events: 0
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
InverterChannelInfo,
|
||||
EventLog,
|
||||
DevInfo,
|
||||
BootstrapAlert,
|
||||
BIconXCircleFill,
|
||||
BIconExclamationCircleFill,
|
||||
BIconCheckCircleFill,
|
||||
BIconSpeedometer,
|
||||
BIconPower,
|
||||
BIconCpu,
|
||||
BIconJournalText,
|
||||
BIconToggleOn,
|
||||
BIconToggleOff,
|
||||
BIconArrowCounterclockwise,
|
||||
VedirectView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
inverterData: [] as Inverter[],
|
||||
isFirstFetchAfterConnect: true,
|
||||
eventLogView: {} as bootstrap.Modal,
|
||||
eventLogList: {},
|
||||
eventLogLoading: true,
|
||||
devInfoView: {} as bootstrap.Modal,
|
||||
devInfoList: {},
|
||||
devInfoLoading: true,
|
||||
|
||||
limitSettingView: {} as bootstrap.Modal,
|
||||
limitSettingSerial: 0,
|
||||
limitSettingLoading: true,
|
||||
|
||||
currentLimit: 0,
|
||||
currentLimitAbsolute: 0,
|
||||
successCommandLimit: "",
|
||||
maxPower: 0,
|
||||
targetLimit: 0,
|
||||
targetLimitMin: 10,
|
||||
targetLimitMax: 100,
|
||||
targetLimitTypeText: "Relative (%)",
|
||||
targetLimitType: 1,
|
||||
targetLimitPersistent: false,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
|
||||
powerSettingView: {} as bootstrap.Modal,
|
||||
powerSettingSerial: 0,
|
||||
powerSettingLoading: true,
|
||||
alertMessagePower: "",
|
||||
alertTypePower: "info",
|
||||
showAlertPower: false,
|
||||
successCommandPower: "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
mounted() {
|
||||
this.eventLogView = new bootstrap.Modal('#eventView');
|
||||
this.devInfoView = new bootstrap.Modal('#devInfoView');
|
||||
this.limitSettingView = new bootstrap.Modal('#limitSettingView');
|
||||
this.powerSettingView = new bootstrap.Modal('#powerSettingView');
|
||||
|
||||
(this.$refs.limitSettingView as HTMLElement).addEventListener("hide.bs.modal", this.onHideLimitSettings);
|
||||
(this.$refs.powerSettingView as HTMLElement).addEventListener("hide.bs.modal", this.onHidePowerSettings);
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
updated() {
|
||||
// Select first tab
|
||||
if (this.isFirstFetchAfterConnect) {
|
||||
this.isFirstFetchAfterConnect = false;
|
||||
|
||||
const firstTabEl = this.$el.querySelector(
|
||||
"#v-pills-tab:first-child button"
|
||||
);
|
||||
if (firstTabEl != null) {
|
||||
const firstTab = new bootstrap.Tab(firstTabEl);
|
||||
firstTab.show();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/livedata/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.inverterData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${host}/livedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.inverterData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the echo websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
this.inverterData.forEach(element => {
|
||||
element.data_age++;
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
onHideEventlog() {
|
||||
this.eventLogView.hide();
|
||||
},
|
||||
onShowEventlog(serial: number) {
|
||||
this.eventLogLoading = true;
|
||||
fetch("/api/eventlog/status?inv=" + serial)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.eventLogList = data[serial];
|
||||
this.eventLogLoading = false;
|
||||
});
|
||||
|
||||
this.eventLogView.show();
|
||||
},
|
||||
onHideDevInfo() {
|
||||
this.devInfoView.hide();
|
||||
},
|
||||
onShowDevInfo(serial: number) {
|
||||
this.devInfoLoading = true;
|
||||
fetch("/api/devinfo/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.devInfoList = data[serial][0];
|
||||
this.devInfoLoading = false;
|
||||
});
|
||||
|
||||
this.devInfoView.show();
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.limitSettingSerial = 0;
|
||||
this.targetLimit = 0;
|
||||
this.targetLimitType = 1;
|
||||
this.targetLimitTypeText = "Relative (%)";
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings(serial: number) {
|
||||
this.limitSettingLoading = true;
|
||||
fetch("/api/limit/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.maxPower = data[serial].max_power;
|
||||
this.currentLimit = Number((data[serial].limit_relative).toFixed(1));
|
||||
if (this.maxPower > 0) {
|
||||
this.currentLimitAbsolute = Number((this.currentLimit * this.maxPower / 100).toFixed(1));
|
||||
}
|
||||
this.successCommandLimit = data[serial].limit_set_status;
|
||||
this.limitSettingSerial = serial;
|
||||
this.limitSettingLoading = false;
|
||||
});
|
||||
|
||||
this.limitSettingView.show();
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
serial: this.limitSettingSerial,
|
||||
limit_value: this.targetLimit,
|
||||
limit_type: (this.targetLimitPersistent ? 256 : 0) + this.targetLimitType,
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(data));
|
||||
|
||||
console.log(data);
|
||||
|
||||
fetch("/api/limit/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.limitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = response.message;
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
onSetLimitSettings(setPersistent: boolean) {
|
||||
this.targetLimitPersistent = setPersistent;
|
||||
},
|
||||
onSelectType(type: number) {
|
||||
if (type == 1) {
|
||||
this.targetLimitTypeText = "Relative (%)";
|
||||
this.targetLimitMin = 10;
|
||||
this.targetLimitMax = 100;
|
||||
} else {
|
||||
this.targetLimitTypeText = "Absolute (W)";
|
||||
this.targetLimitMin = 10;
|
||||
this.targetLimitMax = 1500;
|
||||
}
|
||||
this.targetLimitType = type;
|
||||
},
|
||||
|
||||
onShowPowerSettings(serial: number) {
|
||||
this.powerSettingLoading = true;
|
||||
fetch("/api/power/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.successCommandPower = data[serial].power_set_status;
|
||||
this.powerSettingSerial = serial;
|
||||
this.powerSettingLoading = false;
|
||||
});
|
||||
this.powerSettingView.show();
|
||||
},
|
||||
|
||||
onHidePowerSettings() {
|
||||
this.powerSettingSerial = 0;
|
||||
this.showAlertPower = false;
|
||||
},
|
||||
|
||||
onSetPowerSettings(turnOn: boolean, restart = false) {
|
||||
let data = {};
|
||||
if (restart) {
|
||||
data = {
|
||||
serial: this.powerSettingSerial,
|
||||
restart: true,
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
serial: this.powerSettingSerial,
|
||||
power: turnOn,
|
||||
};
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(data));
|
||||
|
||||
console.log(data);
|
||||
|
||||
fetch("/api/power/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.powerSettingView.hide();
|
||||
} else {
|
||||
this.alertMessagePower = response.message;
|
||||
this.alertTypePower = response.type;
|
||||
this.showAlertPower = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -9,11 +9,11 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<td>{{ ap_ip }}</td>
|
||||
<td>{{ networkStatus.ap_ip }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>MAC Address</th>
|
||||
<td>{{ ap_mac }}</td>
|
||||
<td>{{ networkStatus.ap_mac }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -23,12 +23,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { NetworkStatus } from '@/types/NetworkStatus';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
ap_ip: String,
|
||||
ap_mac: String,
|
||||
networkStatus: { type: Object as PropType<NetworkStatus>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">
|
||||
Network Interface ({{ network_mode }})
|
||||
Network Interface ({{ networkStatus.network_mode }})
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
@ -9,31 +9,31 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td>{{ network_hostname }}</td>
|
||||
<td>{{ networkStatus.network_hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<td>{{ network_ip }}</td>
|
||||
<td>{{ networkStatus.network_ip }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Netmask</th>
|
||||
<td>{{ network_netmask }}</td>
|
||||
<td>{{ networkStatus.network_netmask }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Default Gateway</th>
|
||||
<td>{{ network_gateway }}</td>
|
||||
<td>{{ networkStatus.network_gateway }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DNS 1</th>
|
||||
<td>{{ network_dns1 }}</td>
|
||||
<td>{{ networkStatus.network_dns1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>DNS 2</th>
|
||||
<td>{{ network_dns2 }}</td>
|
||||
<td>{{ networkStatus.network_dns2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>MAC Address</th>
|
||||
<td>{{ network_mac }}</td>
|
||||
<td>{{ networkStatus.network_mac }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -43,18 +43,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { NetworkStatus } from '@/types/NetworkStatus';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
network_hostname: String,
|
||||
network_ip: String,
|
||||
network_netmask: String,
|
||||
network_gateway: String,
|
||||
network_dns1: String,
|
||||
network_dns2: String,
|
||||
network_mac: String,
|
||||
network_mode: String,
|
||||
networkStatus: { type: Object as PropType<NetworkStatus>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -13,9 +13,11 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(property, key) in channelData" :key="`prop-${key}`">
|
||||
<th scope="row">{{ key }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(property.v) }}</td>
|
||||
<td>{{ property.u }}</td>
|
||||
<template v-if="property">
|
||||
<th scope="row">{{ key }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(property.v) }}</td>
|
||||
<td>{{ property.u }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -24,18 +26,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import type { InverterStatistics } from '@/types/LiveDataStatus';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
channelData: Object,
|
||||
channelData: { type: Object as PropType<InverterStatistics>, required: true },
|
||||
channelNumber: { type: Number, required: true },
|
||||
},
|
||||
methods: {
|
||||
formatNumber(num: string) {
|
||||
formatNumber(num: number) {
|
||||
return new Intl.NumberFormat(
|
||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||
).format(parseFloat(num));
|
||||
).format(num);
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -14,10 +14,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<FsInfo name="Heap" :total="heap_total" :used="heap_used" />
|
||||
<FsInfo name="LittleFs" :total="littlefs_total"
|
||||
:used="littlefs_used" />
|
||||
<FsInfo name="Sketch" :total="sketch_total" :used="sketch_used" />
|
||||
<FsInfo name="Heap" :total="systemStatus.heap_total" :used="systemStatus.heap_used" />
|
||||
<FsInfo name="LittleFs" :total="systemStatus.littlefs_total"
|
||||
:used="systemStatus.littlefs_used" />
|
||||
<FsInfo name="Sketch" :total="systemStatus.sketch_total" :used="systemStatus.sketch_used" />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -26,20 +26,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FsInfo from "@/components/partials/FsInfo.vue";
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import type { SystemStatus } from '@/types/SystemStatus';
|
||||
import FsInfo from "@/components/FsInfo.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FsInfo,
|
||||
},
|
||||
props: {
|
||||
heap_total: { type: Number, required: true },
|
||||
heap_used: { type: Number, required: true },
|
||||
littlefs_total: { type: Number, required: true },
|
||||
littlefs_used: { type: Number, required: true },
|
||||
sketch_total: { type: Number, required: true },
|
||||
sketch_used: { type: Number, required: true },
|
||||
systemStatus: { type: Object as PropType<SystemStatus>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,304 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>MqTT Settings</h1>
|
||||
</div>
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<form @submit="saveMqttConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">MqTT Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-4 form-check-label" for="inputMqtt">Enable MqTT</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputMqtt"
|
||||
v-model="mqttConfigList.mqtt_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" v-show="mqttConfigList.mqtt_enabled">
|
||||
<label class="col-sm-4 form-check-label" for="inputMqttHass">Enable Home Assistant MQTT Auto
|
||||
Discovery</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputMqttHass"
|
||||
v-model="mqttConfigList.mqtt_hass_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled">
|
||||
<div class="card-header text-white bg-primary">
|
||||
MqTT Broker Parameter
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputHostname" class="col-sm-2 col-form-label">Hostname:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputHostname" maxlength="128"
|
||||
placeholder="Hostname or IP address" v-model="mqttConfigList.mqtt_hostname" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPort" class="col-sm-2 col-form-label">Port:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="inputPort" min="1" max="65535"
|
||||
placeholder="Port number" v-model="mqttConfigList.mqtt_port" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputUsername" class="col-sm-2 col-form-label">Username:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputUsername" maxlength="32"
|
||||
placeholder="Username, leave empty for anonymous connection"
|
||||
v-model="mqttConfigList.mqtt_username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPassword" class="col-sm-2 col-form-label">Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPassword" maxlength="32"
|
||||
placeholder="Password, leave empty for anonymous connection"
|
||||
v-model="mqttConfigList.mqtt_password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTopic" class="col-sm-2 col-form-label">Base Topic:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputTopic" maxlength="32"
|
||||
placeholder="Base topic, will be prepend to all published topics (e.g. inverter/)"
|
||||
v-model="mqttConfigList.mqtt_topic" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPublishInterval" class="col-sm-2 col-form-label">Publish Interval:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputPublishInterval" min="5"
|
||||
max="86400" placeholder="Publish Interval in Seconds"
|
||||
v-model="mqttConfigList.mqtt_publish_interval"
|
||||
aria-describedby="publishIntervalDescription" />
|
||||
<span class="input-group-text" id="publishIntervalDescription">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputRetain">Enable Retain Flag</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputRetain"
|
||||
v-model="mqttConfigList.mqtt_retain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputTls">Enable TLS</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputTls"
|
||||
v-model="mqttConfigList.mqtt_tls" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" v-show="mqttConfigList.mqtt_tls">
|
||||
<label for="inputCert" class="col-sm-2 col-form-label">CA-Root-Certificate (default
|
||||
Letsencrypt):</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="inputCert" maxlength="2048" rows="10"
|
||||
placeholder="Root CA Certificate from Letsencrypt"
|
||||
v-model="mqttConfigList.mqtt_root_ca_cert">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled">
|
||||
<div class="card-header text-white bg-primary">LWT Parameters</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputLwtTopic" class="col-sm-2 col-form-label">LWT Topic:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" id="basic-addon3">{{
|
||||
mqttConfigList.mqtt_topic
|
||||
}}</span>
|
||||
<input type="text" class="form-control" id="inputLwtTopic" maxlength="32"
|
||||
placeholder="LWT topic, will be append base topic"
|
||||
v-model="mqttConfigList.mqtt_lwt_topic" aria-describedby="basic-addon3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputLwtOnline" class="col-sm-2 col-form-label">LWT Online message:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputLwtOnline" maxlength="20"
|
||||
placeholder="Message that will be published to LWT topic when online"
|
||||
v-model="mqttConfigList.mqtt_lwt_online" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputLwtOffline" class="col-sm-2 col-form-label">LWT Offline message:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputLwtOffline" maxlength="20"
|
||||
placeholder="Message that will be published to LWT topic when offline"
|
||||
v-model="mqttConfigList.mqtt_lwt_offline" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled && mqttConfigList.mqtt_hass_enabled">
|
||||
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Parameters</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputHassTopic" class="col-sm-2 col-form-label">Prefix Topic:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputHassTopic" maxlength="32"
|
||||
placeholder="The prefix for the discovery topic"
|
||||
v-model="mqttConfigList.mqtt_hass_topic" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputHassRetain">Enable Retain Flag</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputHassRetain"
|
||||
v-model="mqttConfigList.mqtt_hass_retain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputHassExpire">Enable Expiration</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputHassExpire"
|
||||
v-model="mqttConfigList.mqtt_hass_expire" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputIndividualPanels">Individual
|
||||
Panels:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputIndividualPanels"
|
||||
v-model="mqttConfigList.mqtt_hass_individualpanels" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
mqttConfigList: {
|
||||
mqtt_enabled: false,
|
||||
mqtt_hostname: "",
|
||||
mqtt_port: 0,
|
||||
mqtt_username: "",
|
||||
mqtt_password: "",
|
||||
mqtt_topic: "",
|
||||
mqtt_publish_interval: 0,
|
||||
mqtt_retain: false,
|
||||
mqtt_tls: false,
|
||||
mqtt_root_ca_cert: "",
|
||||
mqtt_lwt_topic: "",
|
||||
mqtt_lwt_online: "",
|
||||
mqtt_lwt_offline: "",
|
||||
mqtt_hass_enabled: false,
|
||||
mqtt_hass_expire: false,
|
||||
mqtt_hass_retain: false,
|
||||
mqtt_hass_topic: "",
|
||||
mqtt_hass_individualpanels: false
|
||||
},
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getMqttConfig();
|
||||
},
|
||||
methods: {
|
||||
getMqttConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/mqtt/config")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.mqttConfigList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
saveMqttConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.mqttConfigList));
|
||||
|
||||
fetch("/api/mqtt/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>MqTT Info</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Configuration Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_enabled,
|
||||
'bg-success': mqttDataList.mqtt_enabled,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_enabled">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<td>{{ mqttDataList.mqtt_hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Port</th>
|
||||
<td>{{ mqttDataList.mqtt_port }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<td>{{ mqttDataList.mqtt_username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Base Topic</th>
|
||||
<td>{{ mqttDataList.mqtt_topic }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Publish Interval</th>
|
||||
<td>{{ mqttDataList.mqtt_publish_interval }} seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Retain</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_retain,
|
||||
'bg-success': mqttDataList.mqtt_retain,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_retain">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>TLS</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_tls,
|
||||
'bg-success': mqttDataList.mqtt_tls,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_tls">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="mqttDataList.mqtt_tls">
|
||||
<th>Root CA Certifcate Info</th>
|
||||
<td>{{ mqttDataList.mqtt_root_ca_cert_info }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Configuration Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_enabled,
|
||||
'bg-success': mqttDataList.mqtt_hass_enabled,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_enabled">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Base Topic</th>
|
||||
<td>{{ mqttDataList.mqtt_hass_topic }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Retain</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_retain,
|
||||
'bg-success': mqttDataList.mqtt_hass_retain,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_retain">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Expire</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_expire,
|
||||
'bg-success': mqttDataList.mqtt_hass_expire,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_expire">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Individual Panels</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_individualpanels,
|
||||
'bg-success': mqttDataList.mqtt_hass_individualpanels,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_individualpanels">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Runtime Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Connection Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_connected,
|
||||
'bg-success': mqttDataList.mqtt_connected,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_connected">connected</span>
|
||||
<span v-else>disconnected</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
mqttDataList: {
|
||||
mqtt_enabled: false,
|
||||
mqtt_hostname: "",
|
||||
mqtt_port: 0,
|
||||
mqtt_username: "",
|
||||
mqtt_topic: "",
|
||||
mqtt_publish_interval: 0,
|
||||
mqtt_retain: false,
|
||||
mqtt_tls: false,
|
||||
mqtt_root_ca_cert_info: "",
|
||||
mqtt_connected: false,
|
||||
mqtt_hass_enabled: false,
|
||||
mqtt_hass_retain: false,
|
||||
mqtt_hass_topic: "",
|
||||
mqtt_hass_individualpanels: false
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getNtpInfo();
|
||||
},
|
||||
methods: {
|
||||
getNtpInfo() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/mqtt/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.mqttDataList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -83,12 +83,14 @@
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$refs.navbarCollapse && this.$refs.navbarCollapse.classList.remove("show");
|
||||
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show");
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Network Settings</h1>
|
||||
</div>
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<form @submit="saveNetworkConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">WiFi Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputSSID" class="col-sm-2 col-form-label">WiFi SSID:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputSSID" maxlength="32" placeholder="SSID"
|
||||
v-model="networkConfigList.ssid" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPassword" class="col-sm-2 col-form-label">WiFi Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPassword" maxlength="64"
|
||||
placeholder="PSK" v-model="networkConfigList.password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputHostname" class="col-sm-2 col-form-label">Hostname:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputHostname" maxlength="32"
|
||||
placeholder="Hostname" v-model="networkConfigList.hostname" />
|
||||
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b> The text <span class="font-monospace">%06X</span> will be replaced
|
||||
with the last 6 digits of the ESP ChipID in hex format.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputDHCP">Enable DHCP</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputDHCP"
|
||||
v-model="networkConfigList.dhcp" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" v-show="!networkConfigList.dhcp">
|
||||
<div class="card-header text-white bg-primary">
|
||||
Static IP Configuration
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputIP" class="col-sm-2 col-form-label">IP Address:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputIP" maxlength="32"
|
||||
placeholder="IP address" v-model="networkConfigList.ipaddress" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputNetmask" class="col-sm-2 col-form-label">Netmask:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputNetmask" maxlength="32"
|
||||
placeholder="Netmask" v-model="networkConfigList.netmask" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputGateway" class="col-sm-2 col-form-label">Default Gateway:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputGateway" maxlength="32"
|
||||
placeholder="Default Gateway" v-model="networkConfigList.gateway" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputDNS1" class="col-sm-2 col-form-label">DNS Server 1:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputDNS1" maxlength="32"
|
||||
placeholder="DNS Server 1" v-model="networkConfigList.dns1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputDNS2" class="col-sm-2 col-form-label">DNS Server 2:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputDNS2" maxlength="32"
|
||||
placeholder="DNS Server 2" v-model="networkConfigList.dns2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
networkConfigList: {
|
||||
ssid: "",
|
||||
password: "",
|
||||
hostname: "",
|
||||
dhcp: false,
|
||||
ipaddress: "",
|
||||
netmask: "",
|
||||
gateway: "",
|
||||
dns1: "",
|
||||
dns2: ""
|
||||
},
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getNetworkConfig();
|
||||
},
|
||||
methods: {
|
||||
getNetworkConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/network/config")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.networkConfigList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
saveNetworkConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.networkConfigList));
|
||||
|
||||
fetch("/api/network/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Network Info</h1>
|
||||
</div>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<WifiStationInfo v-bind="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<WifiApInfo v-bind="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<InterfaceNetworkInfo v-bind="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<InterfaceApInfo v-bind="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import WifiStationInfo from "./partials/WifiStationInfo.vue";
|
||||
import WifiApInfo from "./partials/WifiApInfo.vue";
|
||||
import InterfaceNetworkInfo from "./partials/InterfaceNetworkInfo.vue";
|
||||
import InterfaceApInfo from "./partials/InterfaceApInfo.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
WifiStationInfo,
|
||||
WifiApInfo,
|
||||
InterfaceNetworkInfo,
|
||||
InterfaceApInfo,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
networkDataList: {
|
||||
// WifiStationInfo
|
||||
sta_status: false,
|
||||
sta_ssid: "",
|
||||
sta_rssi: 0,
|
||||
// WifiApInfo
|
||||
ap_status: false,
|
||||
ap_ssid: "",
|
||||
ap_stationnum: 0,
|
||||
// InterfaceNetworkInfo
|
||||
network_hostname: "",
|
||||
network_ip: "",
|
||||
network_netmask: "",
|
||||
network_gateway: "",
|
||||
network_dns1: "",
|
||||
network_dns2: "",
|
||||
network_mac: "",
|
||||
network_mode: "",
|
||||
// InterfaceApInfo
|
||||
ap_ip: "",
|
||||
ap_mac: "",
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getNetworkInfo();
|
||||
},
|
||||
methods: {
|
||||
getNetworkInfo() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/network/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.networkDataList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>NTP Info</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Configuration Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<td>{{ ntpDataList.ntp_server }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Timezone</th>
|
||||
<td>{{ ntpDataList.ntp_timezone }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Timezone Description</th>
|
||||
<td>{{ ntpDataList.ntp_timezone_descr }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Current Time</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !ntpDataList.ntp_status,
|
||||
'bg-success': ntpDataList.ntp_status,
|
||||
}">
|
||||
<span v-if="ntpDataList.ntp_status">synced</span>
|
||||
<span v-else>not synced</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Local Time</th>
|
||||
<td>{{ ntpDataList.ntp_localtime }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
ntpDataList: {
|
||||
ntp_server: "",
|
||||
ntp_timezone: "",
|
||||
ntp_timezone_descr: "",
|
||||
ntp_status: false,
|
||||
ntp_localtime: ""
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getNtpInfo();
|
||||
},
|
||||
methods: {
|
||||
getNtpInfo() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/ntp/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.ntpDataList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -10,22 +10,24 @@
|
||||
<tr>
|
||||
<th>Chip Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !radio_connected,
|
||||
'bg-success': radio_connected,
|
||||
'bg-danger': !systemStatus.radio_connected,
|
||||
'bg-success': systemStatus.radio_connected,
|
||||
}">
|
||||
<span v-if="radio_connected">connected</span>
|
||||
<span v-if="systemStatus.radio_connected">connected</span>
|
||||
<span v-else>not connected</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Chip Type</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': radio_connected && !radio_pvariant,
|
||||
'bg-success': radio_connected && radio_pvariant,
|
||||
'bg-secondary': !radio_connected,
|
||||
'bg-danger': systemStatus.radio_connected && !systemStatus.radio_pvariant,
|
||||
'bg-success': systemStatus.radio_connected && systemStatus.radio_pvariant,
|
||||
'bg-secondary': !systemStatus.radio_connected,
|
||||
}">
|
||||
<span v-if="radio_connected && radio_pvariant">nRF24L01+</span>
|
||||
<span v-else-if="radio_connected && !radio_pvariant">nRF24L01</span>
|
||||
<span
|
||||
v-if="systemStatus.radio_connected && systemStatus.radio_pvariant">nRF24L01+</span>
|
||||
<span
|
||||
v-else-if="systemStatus.radio_connected && !systemStatus.radio_pvariant">nRF24L01</span>
|
||||
<span v-else>Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
@ -37,12 +39,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import type { SystemStatus } from '@/types/SystemStatus';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
radio_connected: { type: Boolean, required: true },
|
||||
radio_pvariant: { type: Boolean, required: true },
|
||||
systemStatus: { type: Object as PropType<SystemStatus>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,123 +0,0 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Security Settings</h1>
|
||||
</div>
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<form @submit="savePasswordConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Admin password</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputPassword" class="col-sm-2 col-form-label">Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPassword" maxlength="64"
|
||||
placeholder="Password" v-model="password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPasswordRepeat" class="col-sm-2 col-form-label">Repeat Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" maxlength="64"
|
||||
placeholder="Password" v-model="passwordRepeat" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b>
|
||||
The administrator password is used to connect to the device when in AP mode.
|
||||
It must be 8..64 characters.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
|
||||
password: "",
|
||||
passwordRepeat: "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getPasswordConfig();
|
||||
},
|
||||
methods: {
|
||||
getPasswordConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/security/password")
|
||||
.then((response) => response.json())
|
||||
.then(
|
||||
(data) => {
|
||||
this.password = data["password"];
|
||||
this.passwordRepeat = this.password;
|
||||
this.dataLoading = false;
|
||||
}
|
||||
);
|
||||
},
|
||||
savePasswordConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.password != this.passwordRepeat) {
|
||||
this.alertMessage = "Passwords are not equal";
|
||||
this.alertType = "warning";
|
||||
this.showAlert = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
const data = {
|
||||
password: this.password
|
||||
}
|
||||
formData.append("data", JSON.stringify(data));
|
||||
|
||||
fetch("/api/security/password", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -10,20 +10,20 @@
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !ap_status,
|
||||
'bg-success': ap_status,
|
||||
'bg-danger': !networkStatus.ap_status,
|
||||
'bg-success': networkStatus.ap_status,
|
||||
}">
|
||||
<span v-if="ap_status">enabled</span>
|
||||
<span v-if="networkStatus.ap_status">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SSID</th>
|
||||
<td>{{ ap_ssid }}</td>
|
||||
<td>{{ networkStatus.ap_ssid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th># Stations</th>
|
||||
<td>{{ ap_stationnum }}</td>
|
||||
<td>{{ networkStatus.ap_stationnum }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -33,13 +33,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { NetworkStatus } from '@/types/NetworkStatus';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
ap_status: { type: Boolean, required: true },
|
||||
ap_ssid: String,
|
||||
ap_stationnum: { type: Number, required: true },
|
||||
networkStatus: { type: Object as PropType<NetworkStatus>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -10,24 +10,24 @@
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !sta_status,
|
||||
'bg-success': sta_status,
|
||||
'bg-danger': !networkStatus.sta_status,
|
||||
'bg-success': networkStatus.sta_status,
|
||||
}">
|
||||
<span v-if="sta_status">enabled</span>
|
||||
<span v-if="networkStatus.sta_status">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SSID</th>
|
||||
<td>{{ sta_ssid }}</td>
|
||||
<td>{{ networkStatus.sta_ssid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Quality</th>
|
||||
<td>{{ getRSSIasQuality(sta_rssi) }} %</td>
|
||||
<td>{{ getRSSIasQuality(networkStatus.sta_rssi) }} %</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>RSSI</th>
|
||||
<td>{{ sta_rssi }}</td>
|
||||
<td>{{ networkStatus.sta_rssi }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -37,13 +37,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { NetworkStatus } from '@/types/NetworkStatus';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
sta_status: { type: Boolean, required: true },
|
||||
sta_ssid: String,
|
||||
sta_rssi: { type: Number, required: true },
|
||||
networkStatus: { type: Object as PropType<NetworkStatus>, required: true },
|
||||
},
|
||||
methods: {
|
||||
getRSSIasQuality(rssi: number) {
|
||||
@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th scope="col">Start</th>
|
||||
<th scope="col">Stop</th>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Message</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="event in eventLogList.count" :key="event">
|
||||
<tr>
|
||||
<td>{{ timeInHours(eventLogList.events[event - 1].start_time) }}</td>
|
||||
<td>{{ timeInHours(eventLogList.events[event - 1].end_time) }}</td>
|
||||
<td>{{ eventLogList.events[event - 1].message_id }}</td>
|
||||
<td>{{ eventLogList.events[event - 1].message }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
declare interface EventData {
|
||||
message_id: number,
|
||||
message: string,
|
||||
start_time: number,
|
||||
end_time: number
|
||||
}
|
||||
|
||||
declare interface EventLogData {
|
||||
count: number,
|
||||
events: { [key: number]: EventData }
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
eventLogList: { type: Object as () => EventLogData, required: true },
|
||||
},
|
||||
computed: {
|
||||
timeInHours() {
|
||||
return (value: number) => {
|
||||
const days = Math.floor(value / (24 * 60 * 60));
|
||||
const secAfterDays = value - days * (24 * 60 * 60);
|
||||
const hours = Math.floor(secAfterDays / (60 * 60));
|
||||
const secAfterHours = secAfterDays - hours * (60 * 60);
|
||||
const minutes = Math.floor(secAfterHours / 60);
|
||||
const seconds = secAfterHours - minutes * 60;
|
||||
|
||||
const dHours = hours > 9 ? hours : "0" + hours;
|
||||
const dMins = minutes > 9 ? minutes : "0" + minutes;
|
||||
const dSecs = seconds > 9 ? seconds : "0" + seconds;
|
||||
|
||||
return dHours + ":" + dMins + ":" + dSecs;
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">
|
||||
Firmware Information
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td>{{ hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>SDK Version</th>
|
||||
<td>{{ sdkversion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Config Version</th>
|
||||
<td>{{ config_version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Firmware Version / Git Hash</th>
|
||||
<td><a :href="'https://github.com/tbnobody/OpenDTU/commits/' + git_hash?.substring(1)"
|
||||
target="_blank">{{ git_hash?.substring(1) }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Firmware Update</th>
|
||||
<td><a :href="update_url" target="_blank"><span class="badge" :class="update_status">{{
|
||||
update_text }}</span></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Reset Reason CPU 0</th>
|
||||
<td>{{ resetreason_0 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Reset Reason CPU 1</th>
|
||||
<td>{{ resetreason_1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Config save count</th>
|
||||
<td>{{ cfgsavecount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Uptime</th>
|
||||
<td>{{ timeInHours(uptime) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
hostname: String,
|
||||
sdkversion: String,
|
||||
config_version: String,
|
||||
git_hash: String,
|
||||
resetreason_0: String,
|
||||
resetreason_1: String,
|
||||
cfgsavecount: { type: Number, required: true },
|
||||
uptime: { type: Number, required: true },
|
||||
update_text: String,
|
||||
update_url: String,
|
||||
update_status: String,
|
||||
},
|
||||
computed: {
|
||||
timeInHours() {
|
||||
return (value: number) => {
|
||||
const days = Math.floor(value / 3600 / 24);
|
||||
const hours = Math.floor((value - days * 3600 * 24) / 3600);
|
||||
const minutes = Math.floor((value - days * 3600 * 24 - hours * 3600) / 60);
|
||||
const seconds = (value - days * 3600 * 24 - hours * 3600 + minutes * 60) % 60;
|
||||
|
||||
const dHours = hours > 9 ? hours : "0" + hours;
|
||||
const dMins = minutes > 9 ? minutes : "0" + minutes;
|
||||
const dSecs = seconds > 9 ? seconds : "0" + seconds;
|
||||
|
||||
return days + " days " + dHours + ":" + dMins + ":" + dSecs;
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -5,4 +5,8 @@ import router from './router'
|
||||
import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import "bootstrap"
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
import { createWebHistory, createRouter, RouteRecordRaw } from 'vue-router';
|
||||
import HomeView from '@/components/HomeView.vue'
|
||||
import AboutView from '@/components/AboutView.vue'
|
||||
import NetworkInfoView from '@/components/NetworkInfoView.vue'
|
||||
import SystemInfoView from '@/components/SystemInfoView.vue'
|
||||
import NtpInfoView from '@/components/NtpInfoView.vue'
|
||||
import NetworkAdminView from '@/components/NetworkAdminView.vue'
|
||||
import NtpAdminView from '@/components/NtpAdminView.vue'
|
||||
import MqttAdminView from '@/components/MqttAdminView.vue'
|
||||
import MqttInfoView from '@/components/MqttInfoView.vue'
|
||||
import InverterAdminView from '@/components/InverterAdminView.vue'
|
||||
import DtuAdminView from '@/components/DtuAdminView.vue'
|
||||
import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue'
|
||||
import ConfigAdminView from '@/components/ConfigAdminView.vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
import AboutView from '@/views/AboutView.vue'
|
||||
import NetworkInfoView from '@/views/NetworkInfoView.vue'
|
||||
import SystemInfoView from '@/views/SystemInfoView.vue'
|
||||
import NtpInfoView from '@/views/NtpInfoView.vue'
|
||||
import NetworkAdminView from '@/views/NetworkAdminView.vue'
|
||||
import NtpAdminView from '@/views/NtpAdminView.vue'
|
||||
import MqttAdminView from '@/views/MqttAdminView.vue'
|
||||
import MqttInfoView from '@/views/MqttInfoView.vue'
|
||||
import InverterAdminView from '@/views/InverterAdminView.vue'
|
||||
import DtuAdminView from '@/views/DtuAdminView.vue'
|
||||
import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue'
|
||||
import ConfigAdminView from '@/views/ConfigAdminView.vue'
|
||||
import VedirectAdminView from '@/components/VedirectAdminView.vue'
|
||||
import VedirectInfoView from '@/components/VedirectInfoView.vue'
|
||||
import SecurityAdminView from '@/components/SecurityAdminView.vue'
|
||||
import SecurityAdminView from '@/views/SecurityAdminView.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
linkActiveClass: "active",
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
@ -97,12 +100,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'Security',
|
||||
component: SecurityAdminView
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
linkActiveClass: "active",
|
||||
});
|
||||
]
|
||||
})
|
||||
|
||||
export default router;
|
||||
6
webapp/src/shims-vue.d.ts
vendored
6
webapp/src/shims-vue.d.ts
vendored
@ -1,6 +0,0 @@
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
9
webapp/src/types/DevInfoStatus.ts
Normal file
9
webapp/src/types/DevInfoStatus.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface DevInfoStatus {
|
||||
valid_data: boolean,
|
||||
fw_bootloader_version: number,
|
||||
fw_build_version: number,
|
||||
fw_build_datetime: Date,
|
||||
hw_part_number: number,
|
||||
hw_version: number,
|
||||
hw_model_name: string
|
||||
}
|
||||
5
webapp/src/types/DtuConfig.ts
Normal file
5
webapp/src/types/DtuConfig.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface DtuConfig {
|
||||
dtu_serial: number,
|
||||
dtu_pollinterval: number,
|
||||
dtu_palevel: number
|
||||
}
|
||||
11
webapp/src/types/EventlogStatus.ts
Normal file
11
webapp/src/types/EventlogStatus.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface EventlogItem {
|
||||
message_id: number,
|
||||
message: string,
|
||||
start_time: number,
|
||||
end_time: number
|
||||
}
|
||||
|
||||
export interface EventlogItems {
|
||||
count: number,
|
||||
events: Array<EventlogItem>,
|
||||
}
|
||||
33
webapp/src/types/LiveDataStatus.ts
Normal file
33
webapp/src/types/LiveDataStatus.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export interface ValueObject {
|
||||
v: number, // value
|
||||
u: string, // unit
|
||||
};
|
||||
|
||||
export interface InverterStatistics {
|
||||
Power?: ValueObject,
|
||||
Voltage?: ValueObject,
|
||||
Current?: ValueObject,
|
||||
"Power DC"?: ValueObject,
|
||||
YieldDay?: ValueObject,
|
||||
YieldTotal?: ValueObject,
|
||||
Frequency?: ValueObject,
|
||||
Temperature?: ValueObject,
|
||||
PowerFactor?: ValueObject,
|
||||
ReactivePower?: ValueObject,
|
||||
Efficiency?: ValueObject,
|
||||
Irradiation?: ValueObject,
|
||||
}
|
||||
|
||||
export interface Inverter {
|
||||
serial: number,
|
||||
name: string,
|
||||
data_age: number,
|
||||
reachable: boolean,
|
||||
producing: boolean,
|
||||
limit_relative: number,
|
||||
limit_absolute: number,
|
||||
events: number,
|
||||
[key: number]: InverterStatistics,
|
||||
};
|
||||
|
||||
export interface Inverters extends Array<Inverter>{};
|
||||
20
webapp/src/types/MqttConfig.ts
Normal file
20
webapp/src/types/MqttConfig.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export interface MqttConfig {
|
||||
mqtt_enabled: boolean,
|
||||
mqtt_hostname: string,
|
||||
mqtt_port: number,
|
||||
mqtt_username: string,
|
||||
mqtt_password: string,
|
||||
mqtt_topic: string,
|
||||
mqtt_publish_interval: number,
|
||||
mqtt_retain: boolean,
|
||||
mqtt_tls: boolean,
|
||||
mqtt_root_ca_cert: string,
|
||||
mqtt_lwt_topic: string,
|
||||
mqtt_lwt_online: string,
|
||||
mqtt_lwt_offline: string,
|
||||
mqtt_hass_enabled: boolean,
|
||||
mqtt_hass_expire: boolean,
|
||||
mqtt_hass_retain: boolean,
|
||||
mqtt_hass_topic: string,
|
||||
mqtt_hass_individualpanels: boolean
|
||||
}
|
||||
17
webapp/src/types/MqttStatus.ts
Normal file
17
webapp/src/types/MqttStatus.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export interface MqttStatus {
|
||||
mqtt_enabled: boolean,
|
||||
mqtt_hostname: string,
|
||||
mqtt_port: number,
|
||||
mqtt_username: string,
|
||||
mqtt_topic: string,
|
||||
mqtt_publish_interval: number,
|
||||
mqtt_retain: boolean,
|
||||
mqtt_tls: boolean,
|
||||
mqtt_root_ca_cert_info: string,
|
||||
mqtt_connected: boolean,
|
||||
mqtt_hass_enabled: boolean,
|
||||
mqtt_hass_expire: boolean,
|
||||
mqtt_hass_retain: boolean,
|
||||
mqtt_hass_topic: string,
|
||||
mqtt_hass_individualpanels: boolean
|
||||
}
|
||||
22
webapp/src/types/NetworkStatus.ts
Normal file
22
webapp/src/types/NetworkStatus.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export interface NetworkStatus {
|
||||
// WifiStationInfo
|
||||
sta_status: boolean,
|
||||
sta_ssid: string,
|
||||
sta_rssi: number,
|
||||
// WifiApInfo
|
||||
ap_status: boolean,
|
||||
ap_ssid: string,
|
||||
ap_stationnum: number,
|
||||
// InterfaceNetworkInfo
|
||||
network_hostname: string,
|
||||
network_ip: string,
|
||||
network_netmask: string,
|
||||
network_gateway: string,
|
||||
network_dns1: string,
|
||||
network_dns2: string,
|
||||
network_mac: string,
|
||||
network_mode: string,
|
||||
// InterfaceApInfo
|
||||
ap_ip: string,
|
||||
ap_mac: string,
|
||||
}
|
||||
11
webapp/src/types/NetworkkConfig.ts
Normal file
11
webapp/src/types/NetworkkConfig.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface NetworkConfig {
|
||||
ssid: string,
|
||||
password: string,
|
||||
hostname: string,
|
||||
dhcp: boolean,
|
||||
ipaddress: string,
|
||||
netmask: string,
|
||||
gateway: string,
|
||||
dns1: string,
|
||||
dns2: string
|
||||
}
|
||||
5
webapp/src/types/NtpConfig.ts
Normal file
5
webapp/src/types/NtpConfig.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface NtpConfig {
|
||||
ntp_server: string,
|
||||
ntp_timezone: string,
|
||||
ntp_timezone_descr: string
|
||||
}
|
||||
7
webapp/src/types/NtpStatus.ts
Normal file
7
webapp/src/types/NtpStatus.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface NtpStatus {
|
||||
ntp_server: string,
|
||||
ntp_timezone: string,
|
||||
ntp_timezone_descr: string
|
||||
ntp_status: boolean,
|
||||
ntp_localtime: string
|
||||
}
|
||||
3
webapp/src/types/SecurityConfig.ts
Normal file
3
webapp/src/types/SecurityConfig.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface SecurityConfig {
|
||||
password: string
|
||||
}
|
||||
29
webapp/src/types/SystemStatus.ts
Normal file
29
webapp/src/types/SystemStatus.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export interface SystemStatus {
|
||||
// HardwareInfo
|
||||
chipmodel: string,
|
||||
chiprevision: number,
|
||||
chipcores: number,
|
||||
cpufreq: number,
|
||||
// FirmwareInfo
|
||||
hostname: string,
|
||||
sdkversion: string,
|
||||
config_version: string,
|
||||
git_hash: string,
|
||||
resetreason_0: string,
|
||||
resetreason_1: string,
|
||||
cfgsavecount: number,
|
||||
uptime: number,
|
||||
update_text: string,
|
||||
update_url: string,
|
||||
update_status: string,
|
||||
// MemoryInfo
|
||||
heap_total: number,
|
||||
heap_used: number,
|
||||
littlefs_total: number,
|
||||
littlefs_used: number,
|
||||
sketch_total: number,
|
||||
sketch_used: number,
|
||||
// RadioInfo
|
||||
radio_connected: boolean,
|
||||
radio_pvariant: boolean,
|
||||
}
|
||||
9
webapp/src/utils/index.ts
Normal file
9
webapp/src/utils/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { timestampToString } from './time';
|
||||
|
||||
export {
|
||||
timestampToString,
|
||||
};
|
||||
|
||||
export default {
|
||||
timestampToString,
|
||||
}
|
||||
17
webapp/src/utils/time.ts
Normal file
17
webapp/src/utils/time.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export const timestampToString = (value: number, includeDays = false): string => {
|
||||
const days = Math.floor(value / (24 * 60 * 60));
|
||||
const secAfterDays = value - days * (24 * 60 * 60);
|
||||
const hours = Math.floor(secAfterDays / (60 * 60));
|
||||
const secAfterHours = secAfterDays - hours * (60 * 60);
|
||||
const minutes = Math.floor(secAfterHours / 60);
|
||||
const seconds = secAfterHours - minutes * 60;
|
||||
|
||||
const dHours = hours > 9 ? hours : "0" + hours;
|
||||
const dMins = minutes > 9 ? minutes : "0" + minutes;
|
||||
const dSecs = seconds > 9 ? seconds : "0" + seconds;
|
||||
|
||||
if (includeDays) {
|
||||
return days + " days " + dHours + ":" + dMins + ":" + dSecs;
|
||||
}
|
||||
return dHours + ":" + dMins + ":" + dSecs;
|
||||
}
|
||||
@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>About OpenDTU</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<BasePage :title="'About OpenDTU'">
|
||||
<div class="accordion" id="accordionExample">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
@ -96,12 +91,13 @@
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import {
|
||||
BIconInfoCircle,
|
||||
BIconActivity,
|
||||
@ -111,6 +107,7 @@ import {
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BIconInfoCircle,
|
||||
BIconActivity,
|
||||
BIconBug,
|
||||
217
webapp/src/views/ConfigAdminView.vue
Normal file
217
webapp/src/views/ConfigAdminView.vue
Normal file
@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<BasePage :title="'Config Management'" :isLoading="loading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Backup: Configuration File Backup</div>
|
||||
<div class="card-body text-center">
|
||||
Backup the configuration file
|
||||
<button class="btn btn-primary" @click="downloadConfig">Backup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Restore: Restore the Configuration File</div>
|
||||
<div class="card-body text-center">
|
||||
|
||||
<div v-if="!uploading && UploadError != ''">
|
||||
<p class="h1 mb-2">
|
||||
<BIconExclamationCircleFill />
|
||||
</p>
|
||||
<span style="vertical-align: middle" class="ml-2">
|
||||
{{ UploadError }}
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-light" @click="clear">
|
||||
<BIconArrowLeft /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading && UploadSuccess">
|
||||
<span class="h1 mb-2">
|
||||
<BIconCheckCircle />
|
||||
</span>
|
||||
<span> Upload Success </span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-primary" @click="clear">
|
||||
<BIconArrowLeft /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading">
|
||||
<div class="form-group pt-2 mt-3">
|
||||
<input class="form-control" type="file" ref="file" accept=".json" @change="uploadConfig" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="uploading">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" :style="{ width: progress + '%' }"
|
||||
v-bind:aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100">
|
||||
{{ progress }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
<b>Note:</b> This operation replaces the configuration file with the restored configuration and
|
||||
restarts OpenDTU to apply all settings.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Initialize: Perform Factory Reset</div>
|
||||
<div class="card-body text-center">
|
||||
|
||||
<button class="btn btn-danger" @click="onFactoryResetModal">Restore Factory-Default Settings
|
||||
</button>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
<b>Note:</b> Click Restore Factory-Default Settings to restore and initialize the
|
||||
factory-default settings and reboot.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<div class="modal" id="factoryReset" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Factory Reset</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete the current configuration and reset all settings to their
|
||||
factory defaults?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onFactoryResetCancel"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" @click="onFactoryResetPerform">Factory
|
||||
Reset!</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import {
|
||||
BIconExclamationCircleFill,
|
||||
BIconArrowLeft,
|
||||
BIconCheckCircle
|
||||
} from 'bootstrap-icons-vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BIconExclamationCircleFill,
|
||||
BIconArrowLeft,
|
||||
BIconCheckCircle,
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalFactoryReset: {} as bootstrap.Modal,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
loading: true,
|
||||
uploading: false,
|
||||
progress: 0,
|
||||
UploadError: "",
|
||||
UploadSuccess: false,
|
||||
file: {} as Blob,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.modalFactoryReset = new bootstrap.Modal('#factoryReset');
|
||||
this.loading = false;
|
||||
},
|
||||
methods: {
|
||||
onFactoryResetModal() {
|
||||
this.modalFactoryReset.show();
|
||||
},
|
||||
onFactoryResetCancel() {
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
onFactoryResetPerform() {
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify({ delete: true }));
|
||||
|
||||
fetch("/api/config/delete", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
)
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
downloadConfig() {
|
||||
const link = document.createElement('a')
|
||||
link.href = "/api/config/get"
|
||||
link.download = 'config.json'
|
||||
link.click()
|
||||
},
|
||||
uploadConfig(event: Event | null) {
|
||||
this.uploading = true;
|
||||
const formData = new FormData();
|
||||
if (event !== null) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.files !== null) {
|
||||
this.file = target.files[0];
|
||||
}
|
||||
}
|
||||
const request = new XMLHttpRequest();
|
||||
request.addEventListener("load", () => {
|
||||
// request.response will hold the response from the server
|
||||
if (request.status === 200) {
|
||||
this.UploadSuccess = true;
|
||||
} else if (request.status !== 500) {
|
||||
this.UploadError = `[HTTP ERROR] ${request.statusText}`;
|
||||
} else {
|
||||
this.UploadError = request.responseText;
|
||||
}
|
||||
this.uploading = false;
|
||||
this.progress = 0;
|
||||
});
|
||||
// Upload progress
|
||||
request.upload.addEventListener("progress", (e) => {
|
||||
this.progress = Math.trunc((e.loaded / e.total) * 100);
|
||||
});
|
||||
request.withCredentials = true;
|
||||
|
||||
formData.append("config", this.file, "config");
|
||||
request.open("post", "/api/config/upload");
|
||||
request.send(formData);
|
||||
},
|
||||
clear() {
|
||||
this.UploadError = "";
|
||||
this.UploadSuccess = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
116
webapp/src/views/DtuAdminView.vue
Normal file
116
webapp/src/views/DtuAdminView.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<BasePage :title="'DTU Settings'" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<form @submit="saveDtuConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">DTU Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputDtuSerial" class="col-sm-2 col-form-label">Serial:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="inputDtuSerial" min="1" max="99999999999"
|
||||
placeholder="DTU Serial" v-model="dtuConfigList.dtu_serial" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPollInterval" class="col-sm-2 col-form-label">Poll Interval:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputPollInterval" min="1" max="86400"
|
||||
placeholder="Poll Interval in Seconds" v-model="dtuConfigList.dtu_pollinterval"
|
||||
aria-describedby="pollIntervalDescription" />
|
||||
<span class="input-group-text" id="pollIntervalDescription">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTimezone" class="col-sm-2 col-form-label">PA Level:</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="dtuConfigList.dtu_palevel">
|
||||
<option v-for="palevel in palevelList" :key="palevel.key" :value="palevel.key">
|
||||
{{ palevel.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import type { DtuConfig } from "@/types/DtuConfig";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
dtuConfigList: {} as DtuConfig,
|
||||
palevelList: [
|
||||
{ key: 0, value: "Minimum (-18 dBm)" },
|
||||
{ key: 1, value: "Low (-12 dBm)" },
|
||||
{ key: 2, value: "High (-6 dBm)" },
|
||||
{ key: 3, value: "Maximum (0 dBm)" },
|
||||
],
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getDtuConfig();
|
||||
},
|
||||
methods: {
|
||||
getDtuConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/dtu/config")
|
||||
.then((response) => response.json())
|
||||
.then(
|
||||
(data) => {
|
||||
this.dtuConfigList = data;
|
||||
this.dataLoading = false;
|
||||
}
|
||||
);
|
||||
},
|
||||
saveDtuConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.dtuConfigList));
|
||||
|
||||
fetch("/api/dtu/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Firmware Upgrade</h1>
|
||||
</div>
|
||||
|
||||
<BasePage :title="'Firmware Upgrade'">
|
||||
<div class="position-relative" v-if="loading">
|
||||
<div class="position-absolute top-50 start-50 translate-middle">
|
||||
<div class="spinner-border" role="status">
|
||||
@ -68,11 +64,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import SparkMD5 from "spark-md5";
|
||||
import {
|
||||
BIconExclamationCircleFill,
|
||||
@ -83,6 +80,7 @@ import {
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BIconExclamationCircleFill,
|
||||
BIconArrowLeft,
|
||||
BIconArrowRepeat,
|
||||
653
webapp/src/views/HomeView.vue
Normal file
653
webapp/src/views/HomeView.vue
Normal file
@ -0,0 +1,653 @@
|
||||
<template>
|
||||
<BasePage :title="'Live Data'" :isLoading="dataLoading" :isWideScreen="true">
|
||||
<div class="row gy-3">
|
||||
<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">
|
||||
<button v-for="inverter in inverterData" :key="inverter.serial" class="nav-link"
|
||||
: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">
|
||||
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
|
||||
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
|
||||
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
|
||||
{{ inverter.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="v-pills-tabContent" :class="{'col-sm-9 col-md-10': inverterData.length > 1,
|
||||
'col-sm-12 col-md-12': inverterData.length == 1 }">
|
||||
<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-header text-white bg-primary d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'bg-danger': !inverter.reachable,
|
||||
'bg-warning': inverter.reachable && !inverter.producing,
|
||||
'bg-primary': inverter.reachable && inverter.producing,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
{{ inverter.name }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
Serial Number: {{ inverter.serial }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
Current Limit: <template v-if="inverter.limit_absolute > -1"> {{
|
||||
inverter.limit_absolute.toFixed(0) }}W | </template>{{
|
||||
inverter.limit_relative.toFixed(0)
|
||||
}}%
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
Data Age: {{ inverter.data_age }} seconds
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
@click="onShowLimitSettings(inverter.serial)" title="Show / Set Inverter Limit">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
@click="onShowPowerSettings(inverter.serial)" title="Turn Inverter on/off">
|
||||
<BIconPower style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-info"
|
||||
@click="onShowDevInfo(inverter.serial)" title="Show Inverter Info">
|
||||
<BIconCpu style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="inverter.events >= 0" type="button"
|
||||
class="btn btn-sm btn-secondary position-relative"
|
||||
@click="onShowEventlog(inverter.serial)" title="Show Eventlog">
|
||||
<BIconJournalText style="font-size:24px;" />
|
||||
<span
|
||||
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
||||
{{ inverter.events }}
|
||||
<span class="visually-hidden">unread messages</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row flex-row-reverse flex-wrap-reverse align-items-end g-3">
|
||||
<template v-for="channel in 5" :key="channel">
|
||||
<div v-if="inverter[channel - 1]" :class="`col order-${5 - channel}`">
|
||||
<InverterChannelInfo :channelData="inverter[channel - 1]"
|
||||
:channelNumber="channel - 1" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<div class="modal" id="eventView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Event Log</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center" v-if="eventLogLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EventLog v-if="!eventLogLoading" :eventLogList="eventLogList" />
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onHideEventlog"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="devInfoView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Inverter Info</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center" v-if="devInfoLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DevInfo v-if="!devInfoLoading" :devInfoList="devInfoList" />
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onHideDevInfo"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="limitSettingView" ref="limitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Limit Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<BootstrapAlert v-model="showAlertLimit" :variant="alertTypeLimit">
|
||||
{{ alertMessageLimit }}
|
||||
</BootstrapAlert>
|
||||
<div class="text-center" v-if="limitSettingLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!limitSettingLoading">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">Current
|
||||
Limit:</label>
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputCurrentLimit"
|
||||
aria-describedby="currentLimitType" v-model="currentLimit" disabled />
|
||||
<span class="input-group-text" id="currentLimitType">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4" v-if="maxPower > 0">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputCurrentLimitAbsolute"
|
||||
aria-describedby="currentLimitTypeAbsolute" v-model="currentLimitAbsolute"
|
||||
disabled />
|
||||
<span class="input-group-text" id="currentLimitTypeAbsolute">W</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputLastLimitSet" class="col-sm-3 col-form-label">Last Limit Set
|
||||
Status:</label>
|
||||
<div class="col-sm-9">
|
||||
<span class="badge" :class="{
|
||||
'bg-danger': successCommandLimit == 'Failure',
|
||||
'bg-warning': successCommandLimit == 'Pending',
|
||||
'bg-success': successCommandLimit == 'Ok',
|
||||
'bg-secondary': successCommandLimit == 'Unknown',
|
||||
}">
|
||||
{{ successCommandLimit }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTargetLimit" class="col-sm-3 col-form-label">Set Limit:</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input type="number" name="inputTargetLimit" class="form-control"
|
||||
id="inputTargetLimit" :min="targetLimitMin" :max="targetLimitMax"
|
||||
v-model="targetLimit">
|
||||
<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">
|
||||
<li><a class="dropdown-item" @click="onSelectType(1)" href="#">Relative
|
||||
(%)</a></li>
|
||||
<li><a class="dropdown-item" @click="onSelectType(0)" href="#">Absolute
|
||||
(W)</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="targetLimitType == 0" class="alert alert-secondary mt-3" role="alert">
|
||||
<b>Hint:</b> If you set the limit as absolute value the display of the
|
||||
current value will only be updated after ~4 minutes.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">Set Limit
|
||||
Persistent</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">Set Limit
|
||||
Non-Persistent</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="powerSettingView" ref="powerSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Power Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<BootstrapAlert v-model="showAlertPower" :variant="alertTypePower">
|
||||
{{ alertMessagePower }}
|
||||
</BootstrapAlert>
|
||||
<div class="text-center" v-if="powerSettingLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!powerSettingLoading">
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputLastPowerSet" class="col col-form-label">Last Power Set
|
||||
Status:</label>
|
||||
<div class="col">
|
||||
<span class="badge" :class="{
|
||||
'bg-danger': successCommandPower == 'Failure',
|
||||
'bg-warning': successCommandPower == 'Pending',
|
||||
'bg-success': successCommandPower == 'Ok',
|
||||
'bg-secondary': successCommandPower == 'Unknown',
|
||||
}">
|
||||
{{ successCommandPower }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 col-6 mx-auto">
|
||||
<button type="button" class="btn btn-success" @click="onSetPowerSettings(true)">
|
||||
<BIconToggleOn class="fs-4" /> Turn On
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" @click="onSetPowerSettings(false)">
|
||||
<BIconToggleOff class="fs-4" /> Turn Off
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" @click="onSetPowerSettings(true, true)">
|
||||
<BIconArrowCounterclockwise class="fs-4" /> Restart
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import {
|
||||
BIconXCircleFill,
|
||||
BIconExclamationCircleFill,
|
||||
BIconCheckCircleFill,
|
||||
BIconSpeedometer,
|
||||
BIconPower,
|
||||
BIconCpu,
|
||||
BIconJournalText,
|
||||
BIconToggleOn,
|
||||
BIconToggleOff,
|
||||
BIconArrowCounterclockwise
|
||||
} from 'bootstrap-icons-vue';
|
||||
import EventLog from '@/components/EventLog.vue';
|
||||
import DevInfo from '@/components/DevInfo.vue';
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import InverterChannelInfo from "@/components/InverterChannelInfo.vue";
|
||||
import type { DevInfoStatus } from '@/types/DevInfoStatus';
|
||||
import type { EventlogItems } from '@/types/EventlogStatus';
|
||||
import type { Inverters } from '@/types/LiveDataStatus';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
InverterChannelInfo,
|
||||
EventLog,
|
||||
DevInfo,
|
||||
BootstrapAlert,
|
||||
BIconXCircleFill,
|
||||
BIconExclamationCircleFill,
|
||||
BIconCheckCircleFill,
|
||||
BIconSpeedometer,
|
||||
BIconPower,
|
||||
BIconCpu,
|
||||
BIconJournalText,
|
||||
BIconToggleOn,
|
||||
BIconToggleOff,
|
||||
BIconArrowCounterclockwise,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
inverterData: [] as Inverters,
|
||||
isFirstFetchAfterConnect: true,
|
||||
eventLogView: {} as bootstrap.Modal,
|
||||
eventLogList: {} as EventlogItems,
|
||||
eventLogLoading: true,
|
||||
devInfoView: {} as bootstrap.Modal,
|
||||
devInfoList: {} as DevInfoStatus,
|
||||
devInfoLoading: true,
|
||||
|
||||
limitSettingView: {} as bootstrap.Modal,
|
||||
limitSettingSerial: 0,
|
||||
limitSettingLoading: true,
|
||||
|
||||
currentLimit: 0,
|
||||
currentLimitAbsolute: 0,
|
||||
successCommandLimit: "",
|
||||
maxPower: 0,
|
||||
targetLimit: 0,
|
||||
targetLimitMin: 10,
|
||||
targetLimitMax: 100,
|
||||
targetLimitTypeText: "Relative (%)",
|
||||
targetLimitType: 1,
|
||||
targetLimitPersistent: false,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
|
||||
powerSettingView: {} as bootstrap.Modal,
|
||||
powerSettingSerial: 0,
|
||||
powerSettingLoading: true,
|
||||
alertMessagePower: "",
|
||||
alertTypePower: "info",
|
||||
showAlertPower: false,
|
||||
successCommandPower: "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
mounted() {
|
||||
this.eventLogView = new bootstrap.Modal('#eventView');
|
||||
this.devInfoView = new bootstrap.Modal('#devInfoView');
|
||||
this.limitSettingView = new bootstrap.Modal('#limitSettingView');
|
||||
this.powerSettingView = new bootstrap.Modal('#powerSettingView');
|
||||
|
||||
(this.$refs.limitSettingView as HTMLElement).addEventListener("hide.bs.modal", this.onHideLimitSettings);
|
||||
(this.$refs.powerSettingView as HTMLElement).addEventListener("hide.bs.modal", this.onHidePowerSettings);
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
updated() {
|
||||
// Select first tab
|
||||
if (this.isFirstFetchAfterConnect) {
|
||||
this.isFirstFetchAfterConnect = false;
|
||||
|
||||
const firstTabEl = document.querySelector(
|
||||
"#v-pills-tab:first-child button"
|
||||
);
|
||||
if (firstTabEl != null) {
|
||||
const firstTab = new bootstrap.Tab(firstTabEl);
|
||||
firstTab.show();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/livedata/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.inverterData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${host}/livedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.inverterData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the echo websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
this.inverterData.forEach(element => {
|
||||
element.data_age++;
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
onHideEventlog() {
|
||||
this.eventLogView.hide();
|
||||
},
|
||||
onShowEventlog(serial: number) {
|
||||
this.eventLogLoading = true;
|
||||
fetch("/api/eventlog/status?inv=" + serial)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.eventLogList = data[serial];
|
||||
this.eventLogLoading = false;
|
||||
});
|
||||
|
||||
this.eventLogView.show();
|
||||
},
|
||||
onHideDevInfo() {
|
||||
this.devInfoView.hide();
|
||||
},
|
||||
onShowDevInfo(serial: number) {
|
||||
this.devInfoLoading = true;
|
||||
fetch("/api/devinfo/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.devInfoList = data[serial][0];
|
||||
this.devInfoLoading = false;
|
||||
});
|
||||
|
||||
this.devInfoView.show();
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.limitSettingSerial = 0;
|
||||
this.targetLimit = 0;
|
||||
this.targetLimitType = 1;
|
||||
this.targetLimitTypeText = "Relative (%)";
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings(serial: number) {
|
||||
this.limitSettingLoading = true;
|
||||
fetch("/api/limit/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.maxPower = data[serial].max_power;
|
||||
this.currentLimit = Number((data[serial].limit_relative).toFixed(1));
|
||||
if (this.maxPower > 0) {
|
||||
this.currentLimitAbsolute = Number((this.currentLimit * this.maxPower / 100).toFixed(1));
|
||||
}
|
||||
this.successCommandLimit = data[serial].limit_set_status;
|
||||
this.limitSettingSerial = serial;
|
||||
this.limitSettingLoading = false;
|
||||
});
|
||||
|
||||
this.limitSettingView.show();
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
serial: this.limitSettingSerial,
|
||||
limit_value: this.targetLimit,
|
||||
limit_type: (this.targetLimitPersistent ? 256 : 0) + this.targetLimitType,
|
||||
};
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(data));
|
||||
|
||||
console.log(data);
|
||||
|
||||
fetch("/api/limit/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.limitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = response.message;
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
onSetLimitSettings(setPersistent: boolean) {
|
||||
this.targetLimitPersistent = setPersistent;
|
||||
},
|
||||
onSelectType(type: number) {
|
||||
if (type == 1) {
|
||||
this.targetLimitTypeText = "Relative (%)";
|
||||
this.targetLimitMin = 10;
|
||||
this.targetLimitMax = 100;
|
||||
} else {
|
||||
this.targetLimitTypeText = "Absolute (W)";
|
||||
this.targetLimitMin = 10;
|
||||
this.targetLimitMax = 1500;
|
||||
}
|
||||
this.targetLimitType = type;
|
||||
},
|
||||
|
||||
onShowPowerSettings(serial: number) {
|
||||
this.powerSettingLoading = true;
|
||||
fetch("/api/power/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.successCommandPower = data[serial].power_set_status;
|
||||
this.powerSettingSerial = serial;
|
||||
this.powerSettingLoading = false;
|
||||
});
|
||||
this.powerSettingView.show();
|
||||
},
|
||||
|
||||
onHidePowerSettings() {
|
||||
this.powerSettingSerial = 0;
|
||||
this.showAlertPower = false;
|
||||
},
|
||||
|
||||
onSetPowerSettings(turnOn: boolean, restart = false) {
|
||||
let data = {};
|
||||
if (restart) {
|
||||
data = {
|
||||
serial: this.powerSettingSerial,
|
||||
restart: true,
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
serial: this.powerSettingSerial,
|
||||
power: turnOn,
|
||||
};
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(data));
|
||||
|
||||
console.log(data);
|
||||
|
||||
fetch("/api/power/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.powerSettingView.hide();
|
||||
} else {
|
||||
this.alertMessagePower = response.message;
|
||||
this.alertTypePower = response.type;
|
||||
this.showAlertPower = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Inverter Settings</h1>
|
||||
</div>
|
||||
|
||||
<BasePage :title="'Inverter Settings'" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
@ -72,86 +68,85 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<div class="modal" id="inverterEdit" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Inverter</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal" id="inverterEdit" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Inverter</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="inverter-serial" class="col-form-label">Serial:</label>
|
||||
<input v-model="editInverterData.serial" type="number" id="inverter-serial"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inverter-name" class="col-form-label">Name:</label>
|
||||
<input v-model="editInverterData.name" type="text" id="inverter-name"
|
||||
class="form-control" maxlength="31" />
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="inverter-serial" class="col-form-label">Serial:</label>
|
||||
<input v-model="editInverterData.serial" type="number" id="inverter-serial"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inverter-name" class="col-form-label">Name:</label>
|
||||
<input v-model="editInverterData.name" type="text" id="inverter-name" class="form-control"
|
||||
maxlength="31" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-for="(max, index) in editInverterData.max_power" :key="`${index}`">
|
||||
<label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index +
|
||||
1
|
||||
}}:</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0"
|
||||
v-model="editInverterData.max_power[index]"
|
||||
:aria-describedby="`inverter-maxDescription_${index} inverter-maxHelpText_${index}`" />
|
||||
<span class="input-group-text" :id="`inverter-maxDescription_${index}`">W</span>
|
||||
</div>
|
||||
<div :id="`inverter-maxHelpText_${index}`" class="form-text">This value is used to
|
||||
calculate the Irradiation.</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mb-3" v-for="(max, index) in editInverterData.max_power" :key="`${index}`">
|
||||
<label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index +
|
||||
1
|
||||
}}:</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0"
|
||||
v-model="editInverterData.max_power[index]"
|
||||
:aria-describedby="`inverter-maxDescription_${index} inverter-maxHelpText_${index}`" />
|
||||
<span class="input-group-text" :id="`inverter-maxDescription_${index}`">W</span>
|
||||
</div>
|
||||
<div :id="`inverter-maxHelpText_${index}`" class="form-text">This value is used to
|
||||
calculate the Irradiation.</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onCancel"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" @click="onEditSubmit(editId)">Save
|
||||
changes</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onCancel"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" @click="onEditSubmit(editId)">Save
|
||||
changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="inverterDelete" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Delete Inverter</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete the inverter "{{ deleteInverterData.name }}" with serial number
|
||||
{{ deleteInverterData.serial }}?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onDeleteCancel"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger"
|
||||
@click="onDelete(deleteId.toString())">Delete</button>
|
||||
</div>
|
||||
<div class="modal" id="inverterDelete" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Delete Inverter</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete the inverter "{{ deleteInverterData.name }}" with serial number
|
||||
{{ deleteInverterData.serial }}?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onDeleteCancel"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" @click="onDelete(deleteId.toString())">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import {
|
||||
BIconTrash,
|
||||
BIconPencil
|
||||
} from 'bootstrap-icons-vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
|
||||
declare interface Inverter {
|
||||
id: string,
|
||||
@ -163,6 +158,7 @@ declare interface Inverter {
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
BIconTrash,
|
||||
BIconPencil,
|
||||
@ -177,6 +173,7 @@ export default defineComponent({
|
||||
editInverterData: {} as Inverter,
|
||||
deleteInverterData: {} as Inverter,
|
||||
inverters: [] as Inverter[],
|
||||
dataLoading: true,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
@ -198,9 +195,13 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
getInverters() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/inverter/list")
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.inverters = data.inverter));
|
||||
.then((data) => {
|
||||
this.inverters = data.inverter;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
onSubmit() {
|
||||
const formData = new FormData();
|
||||
277
webapp/src/views/MqttAdminView.vue
Normal file
277
webapp/src/views/MqttAdminView.vue
Normal file
@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<BasePage :title="'MqTT Settings'" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<form @submit="saveMqttConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">MqTT Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-4 form-check-label" for="inputMqtt">Enable MqTT</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputMqtt"
|
||||
v-model="mqttConfigList.mqtt_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" v-show="mqttConfigList.mqtt_enabled">
|
||||
<label class="col-sm-4 form-check-label" for="inputMqttHass">Enable Home Assistant MQTT Auto
|
||||
Discovery</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputMqttHass"
|
||||
v-model="mqttConfigList.mqtt_hass_enabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled">
|
||||
<div class="card-header text-white bg-primary">
|
||||
MqTT Broker Parameter
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputHostname" class="col-sm-2 col-form-label">Hostname:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputHostname" maxlength="128"
|
||||
placeholder="Hostname or IP address" v-model="mqttConfigList.mqtt_hostname" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPort" class="col-sm-2 col-form-label">Port:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="inputPort" min="1" max="65535"
|
||||
placeholder="Port number" v-model="mqttConfigList.mqtt_port" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputUsername" class="col-sm-2 col-form-label">Username:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputUsername" maxlength="32"
|
||||
placeholder="Username, leave empty for anonymous connection"
|
||||
v-model="mqttConfigList.mqtt_username" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPassword" class="col-sm-2 col-form-label">Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPassword" maxlength="32"
|
||||
placeholder="Password, leave empty for anonymous connection"
|
||||
v-model="mqttConfigList.mqtt_password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTopic" class="col-sm-2 col-form-label">Base Topic:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputTopic" maxlength="32"
|
||||
placeholder="Base topic, will be prepend to all published topics (e.g. inverter/)"
|
||||
v-model="mqttConfigList.mqtt_topic" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPublishInterval" class="col-sm-2 col-form-label">Publish Interval:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" id="inputPublishInterval" min="5" max="86400"
|
||||
placeholder="Publish Interval in Seconds"
|
||||
v-model="mqttConfigList.mqtt_publish_interval"
|
||||
aria-describedby="publishIntervalDescription" />
|
||||
<span class="input-group-text" id="publishIntervalDescription">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputRetain">Enable Retain Flag</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputRetain"
|
||||
v-model="mqttConfigList.mqtt_retain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputTls">Enable TLS</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputTls"
|
||||
v-model="mqttConfigList.mqtt_tls" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" v-show="mqttConfigList.mqtt_tls">
|
||||
<label for="inputCert" class="col-sm-2 col-form-label">CA-Root-Certificate (default
|
||||
Letsencrypt):</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="inputCert" maxlength="2048" rows="10"
|
||||
placeholder="Root CA Certificate from Letsencrypt"
|
||||
v-model="mqttConfigList.mqtt_root_ca_cert">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled">
|
||||
<div class="card-header text-white bg-primary">LWT Parameters</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputLwtTopic" class="col-sm-2 col-form-label">LWT Topic:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" id="basic-addon3">{{
|
||||
mqttConfigList.mqtt_topic
|
||||
}}</span>
|
||||
<input type="text" class="form-control" id="inputLwtTopic" maxlength="32"
|
||||
placeholder="LWT topic, will be append base topic"
|
||||
v-model="mqttConfigList.mqtt_lwt_topic" aria-describedby="basic-addon3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputLwtOnline" class="col-sm-2 col-form-label">LWT Online message:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputLwtOnline" maxlength="20"
|
||||
placeholder="Message that will be published to LWT topic when online"
|
||||
v-model="mqttConfigList.mqtt_lwt_online" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputLwtOffline" class="col-sm-2 col-form-label">LWT Offline message:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputLwtOffline" maxlength="20"
|
||||
placeholder="Message that will be published to LWT topic when offline"
|
||||
v-model="mqttConfigList.mqtt_lwt_offline" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5" v-show="mqttConfigList.mqtt_enabled && mqttConfigList.mqtt_hass_enabled">
|
||||
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Parameters</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputHassTopic" class="col-sm-2 col-form-label">Prefix Topic:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputHassTopic" maxlength="32"
|
||||
placeholder="The prefix for the discovery topic"
|
||||
v-model="mqttConfigList.mqtt_hass_topic" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputHassRetain">Enable Retain Flag</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputHassRetain"
|
||||
v-model="mqttConfigList.mqtt_hass_retain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputHassExpire">Enable Expiration</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputHassExpire"
|
||||
v-model="mqttConfigList.mqtt_hass_expire" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputIndividualPanels">Individual
|
||||
Panels:</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputIndividualPanels"
|
||||
v-model="mqttConfigList.mqtt_hass_individualpanels" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import type { MqttConfig } from "@/types/MqttConfig";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
mqttConfigList: {} as MqttConfig,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getMqttConfig();
|
||||
},
|
||||
methods: {
|
||||
getMqttConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/mqtt/config")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.mqttConfigList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
saveMqttConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.mqttConfigList));
|
||||
|
||||
fetch("/api/mqtt/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
180
webapp/src/views/MqttInfoView.vue
Normal file
180
webapp/src/views/MqttInfoView.vue
Normal file
@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<BasePage :title="'MqTT Info'" :isLoading="dataLoading">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Configuration Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_enabled,
|
||||
'bg-success': mqttDataList.mqtt_enabled,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_enabled">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<td>{{ mqttDataList.mqtt_hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Port</th>
|
||||
<td>{{ mqttDataList.mqtt_port }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<td>{{ mqttDataList.mqtt_username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Base Topic</th>
|
||||
<td>{{ mqttDataList.mqtt_topic }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Publish Interval</th>
|
||||
<td>{{ mqttDataList.mqtt_publish_interval }} seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Retain</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_retain,
|
||||
'bg-success': mqttDataList.mqtt_retain,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_retain">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>TLS</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_tls,
|
||||
'bg-success': mqttDataList.mqtt_tls,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_tls">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-show="mqttDataList.mqtt_tls">
|
||||
<th>Root CA Certifcate Info</th>
|
||||
<td>{{ mqttDataList.mqtt_root_ca_cert_info }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Home Assistant MQTT Auto Discovery Configuration Summary
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_enabled,
|
||||
'bg-success': mqttDataList.mqtt_hass_enabled,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_enabled">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Base Topic</th>
|
||||
<td>{{ mqttDataList.mqtt_hass_topic }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Retain</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_retain,
|
||||
'bg-success': mqttDataList.mqtt_hass_retain,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_retain">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Expire</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_expire,
|
||||
'bg-success': mqttDataList.mqtt_hass_expire,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_expire">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Individual Panels</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_hass_individualpanels,
|
||||
'bg-success': mqttDataList.mqtt_hass_individualpanels,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_hass_individualpanels">enabled</span>
|
||||
<span v-else>disabled</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Runtime Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Connection Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !mqttDataList.mqtt_connected,
|
||||
'bg-success': mqttDataList.mqtt_connected,
|
||||
}">
|
||||
<span v-if="mqttDataList.mqtt_connected">connected</span>
|
||||
<span v-else>disconnected</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import type { MqttStatus } from '@/types/MqttStatus';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
mqttDataList: {} as MqttStatus,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getMqttInfo();
|
||||
},
|
||||
methods: {
|
||||
getMqttInfo() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/mqtt/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.mqttDataList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
163
webapp/src/views/NetworkAdminView.vue
Normal file
163
webapp/src/views/NetworkAdminView.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<BasePage :title="'Network Settings'" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<form @submit="saveNetworkConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">WiFi Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputSSID" class="col-sm-2 col-form-label">WiFi SSID:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputSSID" maxlength="32" placeholder="SSID"
|
||||
v-model="networkConfigList.ssid" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPassword" class="col-sm-2 col-form-label">WiFi Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPassword" maxlength="64"
|
||||
placeholder="PSK" v-model="networkConfigList.password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputHostname" class="col-sm-2 col-form-label">Hostname:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputHostname" maxlength="32"
|
||||
placeholder="Hostname" v-model="networkConfigList.hostname" />
|
||||
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b> The text <span class="font-monospace">%06X</span> will be replaced
|
||||
with the last 6 digits of the ESP ChipID in hex format.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputDHCP">Enable DHCP</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputDHCP"
|
||||
v-model="networkConfigList.dhcp" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" v-show="!networkConfigList.dhcp">
|
||||
<div class="card-header text-white bg-primary">
|
||||
Static IP Configuration
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputIP" class="col-sm-2 col-form-label">IP Address:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputIP" maxlength="32" placeholder="IP address"
|
||||
v-model="networkConfigList.ipaddress" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputNetmask" class="col-sm-2 col-form-label">Netmask:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputNetmask" maxlength="32"
|
||||
placeholder="Netmask" v-model="networkConfigList.netmask" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputGateway" class="col-sm-2 col-form-label">Default Gateway:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputGateway" maxlength="32"
|
||||
placeholder="Default Gateway" v-model="networkConfigList.gateway" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputDNS1" class="col-sm-2 col-form-label">DNS Server 1:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputDNS1" maxlength="32"
|
||||
placeholder="DNS Server 1" v-model="networkConfigList.dns1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputDNS2" class="col-sm-2 col-form-label">DNS Server 2:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputDNS2" maxlength="32"
|
||||
placeholder="DNS Server 2" v-model="networkConfigList.dns2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import type { NetworkConfig } from "@/types/NetworkkConfig";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
networkConfigList: {} as NetworkConfig,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getNetworkConfig();
|
||||
},
|
||||
methods: {
|
||||
getNetworkConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/network/config")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.networkConfigList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
saveNetworkConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.networkConfigList));
|
||||
|
||||
fetch("/api/network/config", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
52
webapp/src/views/NetworkInfoView.vue
Normal file
52
webapp/src/views/NetworkInfoView.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<BasePage :title="'Network Info'" :isLoading="dataLoading">
|
||||
<WifiStationInfo :networkStatus="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<WifiApInfo :networkStatus="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<InterfaceNetworkInfo :networkStatus="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<InterfaceApInfo :networkStatus="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import WifiStationInfo from "@/components/WifiStationInfo.vue";
|
||||
import WifiApInfo from "@/components/WifiApInfo.vue";
|
||||
import InterfaceNetworkInfo from "@/components/InterfaceNetworkInfo.vue";
|
||||
import InterfaceApInfo from "@/components/InterfaceApInfo.vue";
|
||||
import type { NetworkStatus } from '@/types/NetworkStatus';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
WifiStationInfo,
|
||||
WifiApInfo,
|
||||
InterfaceNetworkInfo,
|
||||
InterfaceApInfo,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
networkDataList: {} as NetworkStatus,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getNetworkInfo();
|
||||
},
|
||||
methods: {
|
||||
getNetworkInfo() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/network/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.networkDataList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,107 +1,92 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>NTP Settings</h1>
|
||||
</div>
|
||||
<BasePage :title="'NTP Settings'" :isLoading="dataLoading || timezoneLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="text-center" v-if="dataLoading || timezoneLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading && !timezoneLoading">
|
||||
<form @submit="saveNtpConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">NTP Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputNtpServer" class="col-sm-2 col-form-label">Time Server:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputNtpServer" maxlength="32"
|
||||
placeholder="Time Server" v-model="ntpConfigList.ntp_server" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTimezone" class="col-sm-2 col-form-label">Timezone:</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="timezoneSelect">
|
||||
<option v-for="(config, name) in timezoneList" :key="name + '---' + config"
|
||||
:value="name + '---' + config">
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTimezoneConfig" class="col-sm-2 col-form-label">Timezone Config:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputTimezoneConfig" maxlength="32"
|
||||
placeholder="Timezone" v-model="ntpConfigList.ntp_timezone" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template v-if="!dataLoading && !timezoneLoading">
|
||||
<form @submit="saveNtpConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Manual Time Synchronization</div>
|
||||
<div class="card-header text-white bg-primary">NTP Configuration</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="currentMcuTime" class="col-sm-2 col-form-label">Current OpenDTU Time:</label>
|
||||
<label for="inputNtpServer" class="col-sm-2 col-form-label">Time Server:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="currentMcuTime" v-model="mcuTime" disabled />
|
||||
<input type="text" class="form-control" id="inputNtpServer" maxlength="32"
|
||||
placeholder="Time Server" v-model="ntpConfigList.ntp_server" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label for="currentLocalTime" class="col-sm-2 col-form-label">Current Local Time:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="currentLocalTime" v-model="localTime"
|
||||
disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mb-3">
|
||||
<button type="button" class="btn btn-danger" @click="setCurrentTime()"
|
||||
title="Synchronize Time">Synchronize Time
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b> You can use the manual time synchronization to set the current time of OpenDTU if
|
||||
no NTP server is available. But be aware, that in case of power cycle the time gets lost. Also
|
||||
the time accurancy can be very bad as it is not resynchronised regularly.
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTimezone" class="col-sm-2 col-form-label">Timezone:</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="timezoneSelect">
|
||||
<option v-for="(config, name) in timezoneList" :key="name + '---' + config"
|
||||
:value="name + '---' + config">
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputTimezoneConfig" class="col-sm-2 col-form-label">Timezone Config:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputTimezoneConfig" maxlength="32"
|
||||
placeholder="Timezone" v-model="ntpConfigList.ntp_timezone" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Manual Time Synchronization</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="currentMcuTime" class="col-sm-2 col-form-label">Current OpenDTU Time:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="currentMcuTime" v-model="mcuTime" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<label for="currentLocalTime" class="col-sm-2 col-form-label">Current Local Time:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="currentLocalTime" v-model="localTime" disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mb-3">
|
||||
<button type="button" class="btn btn-danger" @click="setCurrentTime()"
|
||||
title="Synchronize Time">Synchronize Time
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b> You can use the manual time synchronization to set the current time of OpenDTU if
|
||||
no NTP server is available. But be aware, that in case of power cycle the time gets lost. Also
|
||||
the time accurancy can be very bad as it is not resynchronised regularly.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import type { NtpConfig } from "@/types/NtpConfig";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
timezoneLoading: true,
|
||||
ntpConfigList: {
|
||||
ntp_server: "",
|
||||
ntp_timezone: "",
|
||||
ntp_timezone_descr: ""
|
||||
},
|
||||
ntpConfigList: {} as NtpConfig,
|
||||
timezoneList: {},
|
||||
timezoneSelect: "",
|
||||
mcuTime: new Date(),
|
||||
85
webapp/src/views/NtpInfoView.vue
Normal file
85
webapp/src/views/NtpInfoView.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<BasePage :title="'NTP Info'" :isLoading="dataLoading">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Configuration Summary</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Server</th>
|
||||
<td>{{ ntpDataList.ntp_server }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Timezone</th>
|
||||
<td>{{ ntpDataList.ntp_timezone }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Timezone Description</th>
|
||||
<td>{{ ntpDataList.ntp_timezone_descr }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Current Time</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td class="badge" :class="{
|
||||
'bg-danger': !ntpDataList.ntp_status,
|
||||
'bg-success': ntpDataList.ntp_status,
|
||||
}">
|
||||
<span v-if="ntpDataList.ntp_status">synced</span>
|
||||
<span v-else>not synced</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Local Time</th>
|
||||
<td>{{ ntpDataList.ntp_localtime }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import type { NtpStatus } from "@/types/NtpStatus";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
ntpDataList: {} as NtpStatus,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getNtpInfo();
|
||||
},
|
||||
methods: {
|
||||
getNtpInfo() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/ntp/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.ntpDataList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
112
webapp/src/views/SecurityAdminView.vue
Normal file
112
webapp/src/views/SecurityAdminView.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<BasePage :title="'Security Settings'" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<form @submit="savePasswordConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Admin password</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<label for="inputPassword" class="col-sm-2 col-form-label">Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPassword" maxlength="64"
|
||||
placeholder="Password" v-model="securityConfigList.password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputPasswordRepeat" class="col-sm-2 col-form-label">Repeat Password:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" maxlength="64"
|
||||
placeholder="Password" v-model="passwordRepeat" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b>
|
||||
The administrator password is used to connect to the device when in AP mode.
|
||||
It must be 8..64 characters.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import type { SecurityConfig } from '@/types/SecurityConfig';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
|
||||
securityConfigList: {} as SecurityConfig,
|
||||
passwordRepeat: "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getPasswordConfig();
|
||||
},
|
||||
methods: {
|
||||
getPasswordConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/security/password")
|
||||
.then((response) => response.json())
|
||||
.then(
|
||||
(data) => {
|
||||
this.securityConfigList = data;
|
||||
this.passwordRepeat = this.securityConfigList.password;
|
||||
this.dataLoading = false;
|
||||
}
|
||||
);
|
||||
},
|
||||
savePasswordConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.securityConfigList.password != this.passwordRepeat) {
|
||||
this.alertMessage = "Passwords are not equal";
|
||||
this.alertType = "warning";
|
||||
this.showAlert = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.securityConfigList));
|
||||
|
||||
fetch("/api/security/password", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,37 +1,28 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>System Info</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!dataLoading">
|
||||
<FirmwareInfo v-bind="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<HardwareInfo v-bind="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<MemoryInfo v-bind="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<RadioInfo v-bind="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
</template>
|
||||
</div>
|
||||
<BasePage :title="'System Info'" :isLoading="dataLoading">
|
||||
<FirmwareInfo :systemStatus="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<HardwareInfo :systemStatus="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<MemoryInfo :systemStatus="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<RadioInfo :systemStatus="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import HardwareInfo from "@/components/partials/HardwareInfo.vue";
|
||||
import FirmwareInfo from "@/components/partials/FirmwareInfo.vue";
|
||||
import MemoryInfo from "@/components/partials/MemoryInfo.vue";
|
||||
import RadioInfo from "@/components/partials/RadioInfo.vue";
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import HardwareInfo from "@/components/HardwareInfo.vue";
|
||||
import FirmwareInfo from "@/components/FirmwareInfo.vue";
|
||||
import MemoryInfo from "@/components/MemoryInfo.vue";
|
||||
import RadioInfo from "@/components/RadioInfo.vue";
|
||||
import type { SystemStatus } from '@/types/SystemStatus';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
HardwareInfo,
|
||||
FirmwareInfo,
|
||||
MemoryInfo,
|
||||
@ -40,35 +31,7 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
systemDataList: {
|
||||
// HardwareInfo
|
||||
chipmodel: "",
|
||||
chiprevision: 0,
|
||||
chipcores: 0,
|
||||
cpufreq: 0,
|
||||
// FirmwareInfo
|
||||
hostname: "",
|
||||
sdkversion: "",
|
||||
config_version: "",
|
||||
git_hash: "",
|
||||
resetreason_0: "",
|
||||
resetreason_1: "",
|
||||
cfgsavecount: 0,
|
||||
uptime: 0,
|
||||
update_text: "",
|
||||
update_url: "",
|
||||
update_status: "",
|
||||
// MemoryInfo
|
||||
heap_total: 0,
|
||||
heap_used: 0,
|
||||
littlefs_total: 0,
|
||||
littlefs_used: 0,
|
||||
sketch_total: 0,
|
||||
sketch_used: 0,
|
||||
// RadioInfo
|
||||
radio_connected: false,
|
||||
radio_pvariant: false,
|
||||
}
|
||||
systemDataList: {} as SystemStatus,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
8
webapp/tsconfig.config.json
Normal file
8
webapp/tsconfig.config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@ -1,41 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
48
webapp/vite.config.ts
Normal file
48
webapp/vite.config.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
viteCompression({ deleteOriginFile: true, threshold: 0 }),
|
||||
cssInjectedByJsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
build: {
|
||||
// Prevent vendor.css being created
|
||||
cssCodeSplit: false,
|
||||
outDir: '../webapp_dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Only create one js file
|
||||
inlineDynamicImports: true,
|
||||
// Get rid of hash on js file
|
||||
entryFileNames: 'js/app.js',
|
||||
// Get rid of hash on css file
|
||||
assetFileNames: "assets/[name].[ext]",
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: 'http://192.168.20.110/'
|
||||
},
|
||||
'^/livedata': {
|
||||
target: 'ws://192.168.20.110/',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,38 +0,0 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
productionSourceMap: false,
|
||||
outputDir: '../webapp_dist',
|
||||
filenameHashing: false,
|
||||
css: {
|
||||
extract: false,
|
||||
},
|
||||
configureWebpack: {
|
||||
optimization: {
|
||||
splitChunks: false
|
||||
}
|
||||
},
|
||||
pluginOptions: {
|
||||
compression: {
|
||||
gzip: {
|
||||
filename: '[file].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
include: /\.(js|css|html|svg|json)(\?.*)?$/i,
|
||||
deleteOriginalAssets: true,
|
||||
minRatio: 0.8,
|
||||
}
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: 'http://192.168.20.110/'
|
||||
},
|
||||
'^/livedata': {
|
||||
target: 'ws://192.168.20.110/',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
5880
webapp/yarn.lock
5880
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user