feat(web-app): plot humidity and temperature

This commit is contained in:
fastium
2025-05-13 17:27:06 +02:00
parent e13170ed1b
commit 899592b493
6 changed files with 409 additions and 69 deletions

View File

@@ -1,53 +1,146 @@
<template> <template>
<div style="text-align: center"> <div class="app-container">
<!-- Header Section -->
<header class="header">
<h1>Home Monitor</h1> <h1>Home Monitor</h1>
<br /> </header>
<!-- <Scatter :data="chartSeries" :options="options" /> -->
<button @click="addTemperature">Add Temperature</button> <!-- 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"
/>
</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> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { import ChartComponent from "./components/ChartComponent.vue";
Chart as ChartJS, import ControlPanel from "./components/ControlPanel.vue";
LinearScale,
PointElement,
LineElement,
Tooltip,
Legend,
} from "chart.js";
import { Scatter } from "vue-chartjs";
import { HttpClient } from "./Services/HttpClient"; import { HttpClient } from "./Services/HttpClient";
import { TimeSeriesManager } from "./TimeSeriesManager"; import { TimeSeriesManager } from "./TimeSeriesManager";
import { URL, USERNAME, PASSWORD, USER, ROOM, DEVICE, APP_NAME } from "./const"; import { URL, USERNAME, PASSWORD, USER, ROOM, DEVICE, APP_NAME } from "./const";
import { Serie } from "./Measures/Serie";
const httpClient = new HttpClient(URL, USERNAME, PASSWORD); let httpClient = new HttpClient(URL, USERNAME, PASSWORD);
const manager = new TimeSeriesManager(httpClient); let manager = new TimeSeriesManager(httpClient);
console.log(manager.getTimeSeriesData(USER, ROOM, DEVICE));
export default defineComponent({ export default defineComponent({
name: APP_NAME, name: APP_NAME,
components: {}, components: {
ChartComponent,
ControlPanel,
},
data() { data() {
return { return {
manager, manager,
series: [] as any[],
loading: true,
error: undefined as string | undefined,
showTemperature: true,
showHumidity: true,
}; };
}, },
methods: { methods: {
addTemperature() {}, addTemperature() {
// Implement your functionality here
console.log("Add temperature clicked");
},
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;
})
.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>

View File

@@ -23,14 +23,16 @@ export class Serie {
} }
public getLabel(): string { public getLabel(): string {
return `${this._user} - ${this._room} - ${this._device}`; return `${this._type} - ${this._user} - ${this._room} - ${this._device}`;
} }
public getSerie(): any { public getSerie(): any {
if (this._type === TEMPERATURE) { if (this._type === TEMPERATURE) {
return { return {
label: this.getLabel(), label: this.getLabel(),
data: this._data, data: this._data.map((v: any) => {
return { x: v.time, y: v.value };
}),
borderColor: "rgba(255, 99, 132, 1)", borderColor: "rgba(255, 99, 132, 1)",
backgroundColor: "rgba(255, 99, 132, 0.2)", backgroundColor: "rgba(255, 99, 132, 0.2)",
borderWidth: 1, borderWidth: 1,
@@ -38,7 +40,9 @@ export class Serie {
} else if (this._type === HUMIDITY) { } else if (this._type === HUMIDITY) {
return { return {
label: this.getLabel(), label: this.getLabel(),
data: this._data, data: this._data.map((v: any) => {
return { x: v.time, y: v.value };
}),
borderColor: "rgba(54, 162, 235, 1)", borderColor: "rgba(54, 162, 235, 1)",
backgroundColor: "rgba(54, 162, 235, 0.2)", backgroundColor: "rgba(54, 162, 235, 0.2)",
borderWidth: 1, borderWidth: 1,

View File

@@ -14,31 +14,63 @@ export class TimeSeriesManager {
user: string, user: string,
room: string, room: string,
device: string device: string
): Promise<{ temperature: Serie; humidity: Serie }> { ): Promise<Serie[]> {
return this.client return this.client
.getValues(user, room, device) .getValues(user, room, device)
.then((response) => { .then((response) => {
console.log(response); // Filter temperature records
const serie_temperature = new Serie( 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, TEMPERATURE,
response.data.filter((x: any) => x[TYPE] === TEMPERATURE), temperatureRecordsProcessed,
user, user,
room, room,
device device
); );
const serie_humidity = new Serie( const humiditySerie = new Serie(
HUMIDITY, HUMIDITY,
response.data.filter((x: any) => x[TYPE] === HUMIDITY), humidityRecordProcessed,
user, user,
room, room,
device device
); );
return { return [temperatureSerie, humiditySerie];
temperature: serie_temperature,
humidity: serie_humidity,
};
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching time series data:", error); console.error("Error fetching time series data:", error);

View File

@@ -1,30 +0,0 @@
<template></template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "ButtonInterface",
props: {
msg: String,
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

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

View File

@@ -0,0 +1,130 @@
<template>
<div class="control-panel">
<button @click="$emit('add-temperature')" class="action-button">
Add Temperature
</button>
<div class="sensor-selector">
<h3>Sensors</h3>
<div class="sensor-options">
<label class="sensor-option">
<input type="checkbox" :checked="showTemperature" />
<span>Temperature</span>
</label>
<label class="sensor-option">
<input type="checkbox" :checked="showHumidity" />
<span>Humidity</span>
</label>
</div>
</div>
<button @click="$emit('refresh')" class="refresh-button">
<span class="button-icon"></span> Refresh Data
</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "ControlPanel",
props: {
showTemperature: {
type: Boolean,
required: true,
},
showHumidity: {
type: Boolean,
required: true,
},
},
emits: [
"add-temperature",
"update:showTemperature",
"update:showHumidity",
"refresh",
"simulate-data",
],
});
</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>