Merge branch 'feat/8-frontend'
Frontend and http client See merge request team-raclette/project-softweng!11
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -92,3 +92,6 @@ fabric.properties
|
|||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
config.json
|
config.json
|
||||||
|
.env
|
||||||
|
|
||||||
|
setup_env.sh
|
||||||
|
|||||||
@@ -90,8 +90,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- RABBITMQ_DEFAULT_USER=$MQTT_USERNAME
|
- RABBITMQ_DEFAULT_USER=$MQTT_USERNAME
|
||||||
- RABBITMQ_DEFAULT_PASS=$MQTT_PASSWORD
|
- RABBITMQ_DEFAULT_PASS=$MQTT_PASSWORD
|
||||||
command:
|
command: sh -c "rabbitmq-plugins enable rabbitmq_mqtt && rabbitmq-server"
|
||||||
sh -c "rabbitmq-plugins enable rabbitmq_mqtt && rabbitmq-server"
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.mqtt-http.entrypoints=http"
|
- "traefik.http.routers.mqtt-http.entrypoints=http"
|
||||||
@@ -101,3 +100,24 @@ services:
|
|||||||
- "traefik.http.routers.mqtt-https.rule=Host(`mqtt.mse.kb28.ch`)"
|
- "traefik.http.routers.mqtt-https.rule=Host(`mqtt.mse.kb28.ch`)"
|
||||||
- "traefik.http.routers.mqtt-https.tls.certResolver=letsencrypt"
|
- "traefik.http.routers.mqtt-https.tls.certResolver=letsencrypt"
|
||||||
- "traefik.http.services.mqtt-https.loadbalancer.server.port=1883"
|
- "traefik.http.services.mqtt-https.loadbalancer.server.port=1883"
|
||||||
|
|
||||||
|
web-app:
|
||||||
|
image: web-app:1.0
|
||||||
|
container_name: web-app
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- VUE_APP_INFLUXDB_USER=$REST_USERNAME
|
||||||
|
- VUE_APP_INFLUXDB_PASSWORD=$REST_PASSWORD
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.web-app-http.entrypoints=http"
|
||||||
|
- "traefik.http.routers.web-app-http.rule=Host(`app.mse.kb28.ch`)"
|
||||||
|
- "traefik.http.middlewares.web-app-redirect.redirectscheme.scheme=https"
|
||||||
|
- "traefik.http.routers.web-app-https.entrypoints=https"
|
||||||
|
- "traefik.http.routers.web-app-https.rule=Host(`app.mse.kb28.ch`)"
|
||||||
|
- "traefik.http.routers.web-app-https.tls.certResolver=letsencrypt"
|
||||||
|
- "traefik.http.services.web-app-https.loadbalancer.server.port=8080"
|
||||||
|
|||||||
66
docs/class-diagram-web-app.puml
Normal file
66
docs/class-diagram-web-app.puml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
@startuml web-app
|
||||||
|
|
||||||
|
skinparam linetype ortho
|
||||||
|
|
||||||
|
class App {
|
||||||
|
' principale vuejs component
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
+getValues()
|
||||||
|
+getNewValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SeriesManager {
|
||||||
|
' get data, put data in graphs, ask new measure
|
||||||
|
|
||||||
|
+getNewValue()
|
||||||
|
+getAllValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SerieConverter {
|
||||||
|
' convert timeseries from backend for graphs
|
||||||
|
+jsonToSerie()
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimeSeries {
|
||||||
|
' simplify the duplication of series in graphs
|
||||||
|
-Series
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Button {
|
||||||
|
' vue js component
|
||||||
|
' ask new measure
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Serie {
|
||||||
|
' contains table of measure
|
||||||
|
' convert the table to graphs format
|
||||||
|
-type
|
||||||
|
-values
|
||||||
|
-numberOfLastValus
|
||||||
|
-id
|
||||||
|
-place
|
||||||
|
-name
|
||||||
|
|
||||||
|
+getGraphFormat()
|
||||||
|
' needs to limit the number of value
|
||||||
|
+addNewValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Plot {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
App *-r- SeriesManager
|
||||||
|
App *-u- Client
|
||||||
|
SeriesManager *-r- TimeSeries
|
||||||
|
Plot -l-* TimeSeries
|
||||||
|
Serie -u-* TimeSeries
|
||||||
|
Serie *-- Button
|
||||||
|
SerieConverter .u.> SeriesManager
|
||||||
|
|
||||||
|
@enduml
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[*.{js,jsx,ts,tsx,vue}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
11
web-app/Dockerfile
Normal file
11
web-app/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:22.15-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["npm", "run", "serve"]
|
||||||
@@ -1,21 +1,11 @@
|
|||||||
# web-app
|
# web-app
|
||||||
|
|
||||||
## Installation
|
|
||||||
Install npm and NodeJS from [here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
|
||||||
|
|
||||||
## Project setup
|
## Project setup
|
||||||
Install all dependencies of the npm project. It needs to have [Vue](https://cli.vuejs.org/guide/installation.html) in global
|
|
||||||
```
|
```
|
||||||
sudo npm install -g @vue/cli
|
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
If you have any problem try the next command :
|
|
||||||
```
|
|
||||||
npm install ./ --legacy-peer-deps
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
### Compiles and hot-reloads for development
|
||||||
Run a preview on localhost
|
|
||||||
```
|
```
|
||||||
npm run serve
|
npm run serve
|
||||||
```
|
```
|
||||||
@@ -25,11 +15,5 @@ npm run serve
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lints and fixes files
|
|
||||||
Run the linter integrated
|
|
||||||
```
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
### Customize configuration
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
|
|||||||
3852
web-app/package-lock.json
generated
3852
web-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,44 +3,23 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve --env-mode",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build"
|
||||||
"lint": "vue-cli-service lint"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"chartjs": "^0.3.24",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-class-component": "^8.0.0-0"
|
"vue-chartjs": "^5.3.2",
|
||||||
|
"vue-multiselect": "^3.2.0",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
|
||||||
"@typescript-eslint/parser": "^5.4.0",
|
|
||||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
|
||||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||||
"@vue/cli-service": "~5.0.0",
|
"@vue/cli-service": "~5.0.0",
|
||||||
"@vue/eslint-config-standard": "^6.1.0",
|
|
||||||
"@vue/eslint-config-typescript": "^9.1.0",
|
|
||||||
"eslint": "^7.32.0",
|
|
||||||
"eslint-plugin-import": "^2.25.3",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-promise": "^5.1.0",
|
|
||||||
"eslint-plugin-vue": "^8.0.3",
|
|
||||||
"typescript": "~4.5.5"
|
"typescript": "~4.5.5"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:vue/vue3-essential",
|
|
||||||
"@vue/standard",
|
|
||||||
"@vue/typescript/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2020
|
|
||||||
},
|
|
||||||
"rules": {}
|
|
||||||
},
|
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
|
|||||||
@@ -1,27 +1,155 @@
|
|||||||
<template>
|
<template>
|
||||||
<img alt="Vue logo" src="./assets/logo.png">
|
<div class="app-container">
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
|
<!-- Header Section -->
|
||||||
|
<header class="header">
|
||||||
|
<h1>Home Monitor</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Content Section -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- Left Sidebar with Controls -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<ControlPanel
|
||||||
|
:show-temperature.sync="showTemperature"
|
||||||
|
:show-humidity.sync="showHumidity"
|
||||||
|
@add-temperature="addTemperature"
|
||||||
|
@refresh="refreshData"
|
||||||
|
:manager="manager"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side Chart Area -->
|
||||||
|
<div class="chart-area">
|
||||||
|
<ChartComponent
|
||||||
|
:series="series"
|
||||||
|
:show-temperature="showTemperature"
|
||||||
|
:show-humidity="showHumidity"
|
||||||
|
:loading="loading"
|
||||||
|
:error="error"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from 'vue-class-component'
|
import { defineComponent } from "vue";
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
import ChartComponent from "./components/ChartComponent.vue";
|
||||||
|
import ControlPanel from "./components/ControlPanel.vue";
|
||||||
|
import { HttpClient } from "./Services/HttpClient";
|
||||||
|
import { TimeSeriesManager } from "./Measures/TimeSeriesManager";
|
||||||
|
import { URL, USERNAME, PASSWORD, USER, ROOM, DEVICE, APP_NAME } from "./const";
|
||||||
|
import { Serie } from "./Measures/Serie";
|
||||||
|
|
||||||
@Options({
|
let httpClient = new HttpClient(URL, USERNAME, PASSWORD);
|
||||||
|
let manager = new TimeSeriesManager(httpClient);
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: APP_NAME,
|
||||||
components: {
|
components: {
|
||||||
HelloWorld
|
ChartComponent,
|
||||||
}
|
ControlPanel,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
manager,
|
||||||
|
series: [] as any[],
|
||||||
|
loading: true,
|
||||||
|
error: undefined as string | undefined,
|
||||||
|
showTemperature: true,
|
||||||
|
showHumidity: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addTemperature() {
|
||||||
|
// Implement your functionality here
|
||||||
|
console.log("Add temperature clicked");
|
||||||
|
manager
|
||||||
|
.getNewValue(USER, ROOM, DEVICE)
|
||||||
|
.then((result) => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error asking data:", error);
|
||||||
|
this.error = "Failed to load data. Please try again later.";
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshData() {
|
||||||
|
console.log("Refreshing data...");
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchData() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = undefined;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.getTimeSeriesData(USER, ROOM, DEVICE)
|
||||||
|
.then((result) => {
|
||||||
|
this.series = result;
|
||||||
|
this.loading = false;
|
||||||
})
|
})
|
||||||
export default class App extends Vue {}
|
.catch((error) => {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
this.error = "Failed to load data. Please try again later.";
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Fetch data when component is mounted
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#app {
|
.app-container {
|
||||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
display: grid;
|
||||||
-webkit-font-smoothing: antialiased;
|
grid-template-rows: auto 1fr;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #2c3e50;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
margin-top: 60px;
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 1fr;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 1px solid #e9ecef;
|
||||||
|
padding: 1.25rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-area {
|
||||||
|
padding: 1.5rem;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
52
web-app/src/Measures/Serie.ts
Normal file
52
web-app/src/Measures/Serie.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { TEMPERATURE, HUMIDITY, TYPE, VALUE } from "../const";
|
||||||
|
|
||||||
|
export class Serie {
|
||||||
|
private _type: string;
|
||||||
|
private _data: { time: number; value: number }[];
|
||||||
|
|
||||||
|
private _user: string;
|
||||||
|
private _room: string;
|
||||||
|
private _device: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
type: string,
|
||||||
|
data: { time: number; value: number }[],
|
||||||
|
user: string,
|
||||||
|
room: string,
|
||||||
|
device: string
|
||||||
|
) {
|
||||||
|
this._type = type;
|
||||||
|
this._data = data;
|
||||||
|
this._user = user;
|
||||||
|
this._room = room;
|
||||||
|
this._device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLabel(): string {
|
||||||
|
return `${this._type} - ${this._user} - ${this._room} - ${this._device}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSerie(): any {
|
||||||
|
if (this._type === TEMPERATURE) {
|
||||||
|
return {
|
||||||
|
label: this.getLabel(),
|
||||||
|
data: this._data.map((v: any) => {
|
||||||
|
return { x: v.time, y: v.value };
|
||||||
|
}),
|
||||||
|
borderColor: "rgba(255, 99, 132, 1)",
|
||||||
|
backgroundColor: "rgba(255, 99, 132, 0.2)",
|
||||||
|
borderWidth: 1,
|
||||||
|
};
|
||||||
|
} else if (this._type === HUMIDITY) {
|
||||||
|
return {
|
||||||
|
label: this.getLabel(),
|
||||||
|
data: this._data.map((v: any) => {
|
||||||
|
return { x: v.time, y: v.value };
|
||||||
|
}),
|
||||||
|
borderColor: "rgba(54, 162, 235, 1)",
|
||||||
|
backgroundColor: "rgba(54, 162, 235, 0.2)",
|
||||||
|
borderWidth: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
web-app/src/Measures/TimeSeriesManager.ts
Normal file
142
web-app/src/Measures/TimeSeriesManager.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { Prop } from "vue";
|
||||||
|
import { Serie } from "./Serie";
|
||||||
|
import { HttpClient } from "../Services/HttpClient";
|
||||||
|
import { TEMPERATURE, HUMIDITY, TYPE, VALUE } from "../const";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export class TimeSeriesManager {
|
||||||
|
private client: HttpClient;
|
||||||
|
|
||||||
|
selected_user = {
|
||||||
|
user: "Rémi",
|
||||||
|
tag: "remi",
|
||||||
|
};
|
||||||
|
selected_room = {
|
||||||
|
room: "Bedroom",
|
||||||
|
tag: "Bedroom",
|
||||||
|
};
|
||||||
|
selected_device = {
|
||||||
|
device: "Door sensor",
|
||||||
|
tag: "DoorSensor",
|
||||||
|
};
|
||||||
|
|
||||||
|
user_options = [
|
||||||
|
{
|
||||||
|
user: "Rémi",
|
||||||
|
tag: "remi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: "Sylvan",
|
||||||
|
tag: "sylvan",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
room_options = [
|
||||||
|
{
|
||||||
|
room: "23N309",
|
||||||
|
tag: "23N309",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
room: "Bedroom",
|
||||||
|
tag: "Bedroom",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
room: "Terrasse",
|
||||||
|
tag: "Terrasse",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
device_options = [
|
||||||
|
{
|
||||||
|
device: "Door sensor",
|
||||||
|
tag: "DoorSensor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device: "Shed",
|
||||||
|
tag: "Shed",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(client: HttpClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeSeriesData(
|
||||||
|
user: string,
|
||||||
|
room: string,
|
||||||
|
device: string
|
||||||
|
): Promise<Serie[]> {
|
||||||
|
return this.client
|
||||||
|
.getValues(
|
||||||
|
this.selected_user.tag,
|
||||||
|
this.selected_room.tag,
|
||||||
|
this.selected_device.tag
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
// Filter temperature records
|
||||||
|
let temperatureRecords = response.data.filter(
|
||||||
|
(x: any) => x[TYPE] === TEMPERATURE
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a new array with type field removed and time converted to timestamp
|
||||||
|
let temperatureRecordsProcessed = temperatureRecords.map((x: any) => {
|
||||||
|
// Create a new object without the TYPE field
|
||||||
|
const { [TYPE]: removed, ...rest } = x;
|
||||||
|
|
||||||
|
// Convert the ISO time string to a timestamp (if it's a string)
|
||||||
|
if (typeof rest.time === "string") {
|
||||||
|
rest.time = new Date(rest.time).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
//filter humidity records
|
||||||
|
let humidityRecord = response.data.filter(
|
||||||
|
(x: any) => x[TYPE] === HUMIDITY
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a new array with type field removed and time converted to timestamp
|
||||||
|
let humidityRecordProcessed = humidityRecord.map((x: any) => {
|
||||||
|
// Create a new object without the TYPE field
|
||||||
|
const { [TYPE]: removed, ...rest } = x;
|
||||||
|
|
||||||
|
// Convert the ISO time string to a timestamp (if it's a string)
|
||||||
|
if (typeof rest.time === "string") {
|
||||||
|
rest.time = new Date(rest.time).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create actual Serie instances
|
||||||
|
const temperatureSerie = new Serie(
|
||||||
|
TEMPERATURE,
|
||||||
|
temperatureRecordsProcessed,
|
||||||
|
user,
|
||||||
|
room,
|
||||||
|
device
|
||||||
|
);
|
||||||
|
|
||||||
|
const humiditySerie = new Serie(
|
||||||
|
HUMIDITY,
|
||||||
|
humidityRecordProcessed,
|
||||||
|
user,
|
||||||
|
room,
|
||||||
|
device
|
||||||
|
);
|
||||||
|
|
||||||
|
return [temperatureSerie, humiditySerie];
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching time series data:", error);
|
||||||
|
throw error; // Re-throw to allow calling code to handle it
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getNewValue(user: string, room: string, device: string) {
|
||||||
|
this.client.newValue(user, room, device).catch((error) => {
|
||||||
|
console.error("Error asking new values:", error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
80
web-app/src/Services/HttpClient.ts
Normal file
80
web-app/src/Services/HttpClient.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
import { BASE, PING, RACLETTE, PONG } from "../const";
|
||||||
|
|
||||||
|
export class HttpClient {
|
||||||
|
private _url: string;
|
||||||
|
private _username: string | undefined;
|
||||||
|
private _password: string | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
url: string,
|
||||||
|
username: string | undefined,
|
||||||
|
password: string | undefined
|
||||||
|
) {
|
||||||
|
this._url = url;
|
||||||
|
this._username = username;
|
||||||
|
this._password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isConnected(): Promise<boolean> {
|
||||||
|
var connected = false;
|
||||||
|
if (await this.ping()) {
|
||||||
|
connected = true;
|
||||||
|
console.log("Connected to backend!");
|
||||||
|
} else {
|
||||||
|
connected = false;
|
||||||
|
console.log("Connection failed backend!");
|
||||||
|
}
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ping(): Promise<string> {
|
||||||
|
const response = await axios.get(`${BASE}${this._url}/${PING}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthHeader() {
|
||||||
|
return {
|
||||||
|
Authorization: `Basic ${btoa(`${this._username}:${this._password}`)}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValues(
|
||||||
|
user: string,
|
||||||
|
room: string,
|
||||||
|
device: string
|
||||||
|
): Promise<AxiosResponse<any, any>> {
|
||||||
|
const response = await axios.get(`${BASE}${this._url}/${RACLETTE}`, {
|
||||||
|
headers: this.getAuthHeader(),
|
||||||
|
params: {
|
||||||
|
user: user,
|
||||||
|
room: room,
|
||||||
|
device: device,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async newValue(
|
||||||
|
user: string,
|
||||||
|
room: string,
|
||||||
|
device: string
|
||||||
|
): Promise<AxiosResponse<any, any>> {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${BASE}${this._url}/${RACLETTE}`,
|
||||||
|
{
|
||||||
|
command: "MEASURE_NEW",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: this.getAuthHeader(),
|
||||||
|
params: {
|
||||||
|
user: user,
|
||||||
|
room: room,
|
||||||
|
device: device,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
web-app/src/components/ChartComponent.vue
Normal file
111
web-app/src/components/ChartComponent.vue
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!-- src/components/ChartComponent.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div v-if="loading" class="loading-state">Loading data...</div>
|
||||||
|
<div v-else-if="error" class="error-state">{{ error }}</div>
|
||||||
|
<div v-else class="chart-container">
|
||||||
|
<Scatter
|
||||||
|
v-if="chartData.datasets.length > 0"
|
||||||
|
:data="chartData"
|
||||||
|
:options="chartOptions"
|
||||||
|
/>
|
||||||
|
<div v-else class="no-data-state">No data to display</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType, computed, proxyRefs } from "vue";
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
} from "chart.js";
|
||||||
|
import { Scatter } from "vue-chartjs";
|
||||||
|
import { Serie } from "../Measures/Serie";
|
||||||
|
|
||||||
|
// Register Chart.js components
|
||||||
|
ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend);
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ChartComponent",
|
||||||
|
components: {
|
||||||
|
Scatter,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
series: {
|
||||||
|
type: Array as PropType<Serie[]>, // Use PropType to specify array of Serie
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showTemperature: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showHumidity: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String as PropType<string | undefined>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const chartData = computed(() => {
|
||||||
|
let series_prepared = props.series.map((s: Serie) => {
|
||||||
|
return s.getSerie();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
datasets: series_prepared,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartOptions,
|
||||||
|
chartData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state,
|
||||||
|
.error-state,
|
||||||
|
.no-data-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
176
web-app/src/components/ControlPanel.vue
Normal file
176
web-app/src/components/ControlPanel.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<div class="control-panel">
|
||||||
|
<button @click="$emit('add-temperature')" class="action-button">
|
||||||
|
Add Temperature
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="$emit('refresh')" class="refresh-button">
|
||||||
|
<span class="button-icon">↻</span> Refresh Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<multiselect
|
||||||
|
id="user-select"
|
||||||
|
v-model="manager.selected_user"
|
||||||
|
:options="manager.user_options"
|
||||||
|
:multiple="false"
|
||||||
|
:group-select="false"
|
||||||
|
placeholder="User"
|
||||||
|
track-by="user"
|
||||||
|
label="user"
|
||||||
|
</multiselect>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<multiselect
|
||||||
|
id="room-select"
|
||||||
|
v-model="manager.selected_room"
|
||||||
|
:options="manager.room_options"
|
||||||
|
:multiple="false"
|
||||||
|
:group-select="false"
|
||||||
|
placeholder="Room"
|
||||||
|
track-by="room"
|
||||||
|
label="room"
|
||||||
|
</multiselect>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<multiselect
|
||||||
|
id="device-select"
|
||||||
|
v-model="manager.selected_device"
|
||||||
|
:options="manager.device_options"
|
||||||
|
:close-on-select="true"
|
||||||
|
:multiple="false"
|
||||||
|
:group-select="false"
|
||||||
|
placeholder="Device"
|
||||||
|
track-by="device"
|
||||||
|
label="device"
|
||||||
|
</multiselect>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import Multiselect from "vue-multiselect";
|
||||||
|
import "vue-multiselect/dist/vue-multiselect.css"; // Add
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { TimeSeriesManager } from "@/Measures/TimeSeriesManager";
|
||||||
|
|
||||||
|
let options = [{ code: "CA", country: "Canada" }];
|
||||||
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ControlPanel",
|
||||||
|
props: {
|
||||||
|
showTemperature: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
showHumidity: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
manager: {
|
||||||
|
type: TimeSeriesManager,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
"add-temperature",
|
||||||
|
"update:showTemperature",
|
||||||
|
"update:showHumidity",
|
||||||
|
"refresh",
|
||||||
|
"simulate-data",
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
Multiselect,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setSelected(value: any) {
|
||||||
|
// trigger a mutation, or dispatch an action
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button,
|
||||||
|
.refresh-button,
|
||||||
|
.demo-button {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
background-color: #3490dc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background-color: #2779bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button {
|
||||||
|
background-color: #38c172;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-button:hover {
|
||||||
|
background-color: #2d995b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button {
|
||||||
|
background-color: #f6993f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button:hover {
|
||||||
|
background-color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-selector h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensor-option input {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
<h3>Installed CLI Plugins</h3>
|
<h3>Installed CLI Plugins</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Essential Links</h3>
|
<h3>Essential Links</h3>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -31,16 +30,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from 'vue-class-component'
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
@Options({
|
export default defineComponent({
|
||||||
|
name: 'HelloWorld',
|
||||||
props: {
|
props: {
|
||||||
msg: String
|
msg: String,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
msg!: string
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
|||||||
21
web-app/src/const.ts
Normal file
21
web-app/src/const.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export const APP_NAME = "Home Monitor";
|
||||||
|
|
||||||
|
export const URL = "rest.mse.kb28.ch";
|
||||||
|
|
||||||
|
// load environment varaibles - need to have the prefix VUE_APP
|
||||||
|
export const USERNAME = process.env.VUE_APP_INFLUXDB_USER;
|
||||||
|
export const PASSWORD = process.env.VUE_APP_INFLUXDB_PASSWORD;
|
||||||
|
|
||||||
|
export const HUMIDITY = "humidity";
|
||||||
|
export const TEMPERATURE = "temperature";
|
||||||
|
export const TYPE = "type";
|
||||||
|
export const VALUE = "value";
|
||||||
|
|
||||||
|
export const USER = "remi";
|
||||||
|
export const ROOM = "Terrasse";
|
||||||
|
export const DEVICE = "Shed";
|
||||||
|
|
||||||
|
export const BASE = "https://";
|
||||||
|
export const PING = "ping";
|
||||||
|
export const RACLETTE = "raclette";
|
||||||
|
export const PONG = "pong";
|
||||||
6
web-app/src/shims-vue.d.ts
vendored
6
web-app/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
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"experimentalDecorators": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user