room-based monitoring
This commit is contained in:
177
src/components/cards/AirQualityCard.vue
Normal file
177
src/components/cards/AirQualityCard.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm p-4">
|
||||||
|
<h6 class="text-sm font-bold text-gray-500 mb-4">Air Quality Status</h6>
|
||||||
|
|
||||||
|
<!-- Overall Status -->
|
||||||
|
<div class="mb-4 p-3 rounded-lg" :class="getOverallStatusBg()">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium" :class="getOverallStatusText()">
|
||||||
|
{{ getOverallStatus() }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm" :class="getOverallStatusText()">
|
||||||
|
Building Average: {{ overallCO2.toFixed(0) }} ppm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 rounded-full flex items-center justify-center" :class="getOverallStatusIconBg()">
|
||||||
|
<svg class="w-6 h-6" :class="getOverallStatusText()" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M10,16.5L6,12.5L7.5,11L10,13.5L16.5,7L18,8.5L10,16.5Z" v-if="overallStatus === 'good'"/>
|
||||||
|
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z" v-else-if="overallStatus === 'moderate'"/>
|
||||||
|
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z" v-else/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Room Status List -->
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div v-if="roomsList.length === 0" class="text-center text-gray-500 py-4">
|
||||||
|
No air quality data available
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="room in roomsList" :key="room.room" class="flex items-center justify-between p-2 rounded">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full"
|
||||||
|
:class="getCO2StatusColor(room.co2.status)"
|
||||||
|
></div>
|
||||||
|
<span class="text-sm font-medium text-gray-900">{{ room.room }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-sm text-gray-900">{{ Math.round(room.co2.current) }} ppm</div>
|
||||||
|
<div class="text-xs" :class="getCO2TextColor(room.co2.status)">
|
||||||
|
{{ room.co2.status.toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="grid grid-cols-2 gap-3 pt-3 border-t border-gray-100">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-lg font-bold text-gray-900">{{ roomsWithGoodAir }}</div>
|
||||||
|
<div class="text-xs text-green-600">Good Air Quality</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-lg font-bold text-gray-900">{{ roomsNeedingAttention }}</div>
|
||||||
|
<div class="text-xs text-orange-600">Need Attention</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recommendations -->
|
||||||
|
<div v-if="recommendations.length > 0" class="mt-4 p-2 bg-blue-50 rounded text-sm">
|
||||||
|
<div class="font-medium text-blue-900 mb-1">Recommendations:</div>
|
||||||
|
<ul class="text-blue-700 text-xs space-y-1">
|
||||||
|
<li v-for="rec in recommendations" :key="rec">• {{ rec }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useEnergyStore } from '@/stores/energy'
|
||||||
|
|
||||||
|
const energyStore = useEnergyStore()
|
||||||
|
|
||||||
|
const roomsList = computed(() => {
|
||||||
|
return Array.from(energyStore.roomsData.values()).sort((a, b) =>
|
||||||
|
b.co2.current - a.co2.current // Sort by CO2 level descending
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallCO2 = computed(() => {
|
||||||
|
if (roomsList.value.length === 0) return 0
|
||||||
|
return roomsList.value.reduce((sum, room) => sum + room.co2.current, 0) / roomsList.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
const overallStatus = computed(() => {
|
||||||
|
return energyStore.getCO2Status(overallCO2.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomsWithGoodAir = computed(() => {
|
||||||
|
return roomsList.value.filter(room => room.co2.status === 'good').length
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomsNeedingAttention = computed(() => {
|
||||||
|
return roomsList.value.filter(room => ['poor', 'critical'].includes(room.co2.status)).length
|
||||||
|
})
|
||||||
|
|
||||||
|
const recommendations = computed(() => {
|
||||||
|
const recs = []
|
||||||
|
const criticalRooms = roomsList.value.filter(room => room.co2.status === 'critical')
|
||||||
|
const poorRooms = roomsList.value.filter(room => room.co2.status === 'poor')
|
||||||
|
|
||||||
|
if (criticalRooms.length > 0) {
|
||||||
|
recs.push(`Immediate ventilation needed in ${criticalRooms[0].room}`)
|
||||||
|
}
|
||||||
|
if (poorRooms.length > 0) {
|
||||||
|
recs.push(`Increase air circulation in ${poorRooms.length} room${poorRooms.length > 1 ? 's' : ''}`)
|
||||||
|
}
|
||||||
|
if (overallCO2.value > 800) {
|
||||||
|
recs.push('Consider adjusting HVAC settings building-wide')
|
||||||
|
}
|
||||||
|
|
||||||
|
return recs.slice(0, 3) // Max 3 recommendations
|
||||||
|
})
|
||||||
|
|
||||||
|
const getOverallStatus = () => {
|
||||||
|
switch (overallStatus.value) {
|
||||||
|
case 'good': return 'Excellent Air Quality'
|
||||||
|
case 'moderate': return 'Moderate Air Quality'
|
||||||
|
case 'poor': return 'Poor Air Quality'
|
||||||
|
case 'critical': return 'Critical - Action Required'
|
||||||
|
default: return 'Unknown Status'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOverallStatusBg = () => {
|
||||||
|
switch (overallStatus.value) {
|
||||||
|
case 'good': return 'bg-green-50 border border-green-200'
|
||||||
|
case 'moderate': return 'bg-yellow-50 border border-yellow-200'
|
||||||
|
case 'poor': return 'bg-orange-50 border border-orange-200'
|
||||||
|
case 'critical': return 'bg-red-50 border border-red-200'
|
||||||
|
default: return 'bg-gray-50 border border-gray-200'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOverallStatusText = () => {
|
||||||
|
switch (overallStatus.value) {
|
||||||
|
case 'good': return 'text-green-700'
|
||||||
|
case 'moderate': return 'text-yellow-700'
|
||||||
|
case 'poor': return 'text-orange-700'
|
||||||
|
case 'critical': return 'text-red-700'
|
||||||
|
default: return 'text-gray-700'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOverallStatusIconBg = () => {
|
||||||
|
switch (overallStatus.value) {
|
||||||
|
case 'good': return 'bg-green-100'
|
||||||
|
case 'moderate': return 'bg-yellow-100'
|
||||||
|
case 'poor': return 'bg-orange-100'
|
||||||
|
case 'critical': return 'bg-red-100'
|
||||||
|
default: return 'bg-gray-100'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCO2StatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'good': return 'bg-green-500'
|
||||||
|
case 'moderate': return 'bg-yellow-500'
|
||||||
|
case 'poor': return 'bg-orange-500'
|
||||||
|
case 'critical': return 'bg-red-500'
|
||||||
|
default: return 'bg-gray-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCO2TextColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'good': return 'text-green-600'
|
||||||
|
case 'moderate': return 'text-yellow-600'
|
||||||
|
case 'poor': return 'text-orange-600'
|
||||||
|
case 'critical': return 'text-red-600'
|
||||||
|
default: return 'text-gray-600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
138
src/components/cards/RoomMetricsCard.vue
Normal file
138
src/components/cards/RoomMetricsCard.vue
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm p-4">
|
||||||
|
<h6 class="text-sm font-bold text-gray-500 mb-4">Room Overview</h6>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div v-if="roomsList.length === 0" class="text-center text-gray-500 py-8">
|
||||||
|
No room data available. Connect sensors to see room metrics.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="room in roomsList" :key="room.room" class="border border-gray-100 rounded-lg p-3">
|
||||||
|
<!-- Room Header -->
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<h3 class="font-medium text-gray-900">{{ room.room }}</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- CO2 Status Indicator -->
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full"
|
||||||
|
:class="getCO2StatusColor(room.co2.status)"
|
||||||
|
></div>
|
||||||
|
<!-- Occupancy Indicator -->
|
||||||
|
<div class="flex items-center gap-1 text-xs text-gray-500">
|
||||||
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="capitalize">{{ room.occupancyEstimate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Metrics Grid -->
|
||||||
|
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<!-- Energy -->
|
||||||
|
<div class="bg-blue-50 rounded p-2">
|
||||||
|
<div class="text-blue-600 font-medium">Energy</div>
|
||||||
|
<div class="text-blue-900">{{ room.energy.current.toFixed(2) }} {{ room.energy.unit }}</div>
|
||||||
|
<div class="text-blue-600 text-xs">Total: {{ room.energy.total.toFixed(2) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CO2 -->
|
||||||
|
<div class="rounded p-2" :class="getCO2BackgroundColor(room.co2.status)">
|
||||||
|
<div class="font-medium" :class="getCO2TextColor(room.co2.status)">CO2</div>
|
||||||
|
<div :class="getCO2TextColor(room.co2.status)">{{ Math.round(room.co2.current) }} {{ room.co2.unit }}</div>
|
||||||
|
<div class="text-xs" :class="getCO2TextColor(room.co2.status)">{{ room.co2.status.toUpperCase() }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sensors Info -->
|
||||||
|
<div class="mt-2 text-xs text-gray-500 flex justify-between">
|
||||||
|
<span>{{ room.sensors.length }} sensor{{ room.sensors.length !== 1 ? 's' : '' }}</span>
|
||||||
|
<span>{{ formatTime(room.lastUpdated) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Stats -->
|
||||||
|
<div v-if="roomsList.length > 0" class="mt-4 pt-4 border-t border-gray-100 grid grid-cols-3 gap-4 text-center text-xs">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-gray-900">{{ roomsList.length }}</div>
|
||||||
|
<div class="text-gray-500">Rooms</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-gray-900">{{ totalEnergy.toFixed(1) }} kWh</div>
|
||||||
|
<div class="text-gray-500">Total Energy</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-gray-900">{{ averageCO2.toFixed(0) }} ppm</div>
|
||||||
|
<div class="text-gray-500">Avg CO2</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useEnergyStore } from '@/stores/energy'
|
||||||
|
|
||||||
|
const energyStore = useEnergyStore()
|
||||||
|
|
||||||
|
const roomsList = computed(() => {
|
||||||
|
return Array.from(energyStore.roomsData.values()).sort((a, b) =>
|
||||||
|
a.room.localeCompare(b.room)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalEnergy = computed(() => {
|
||||||
|
return roomsList.value.reduce((sum, room) => sum + room.energy.current, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const averageCO2 = computed(() => {
|
||||||
|
if (roomsList.value.length === 0) return 0
|
||||||
|
return roomsList.value.reduce((sum, room) => sum + room.co2.current, 0) / roomsList.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCO2StatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'good': return 'bg-green-500'
|
||||||
|
case 'moderate': return 'bg-yellow-500'
|
||||||
|
case 'poor': return 'bg-orange-500'
|
||||||
|
case 'critical': return 'bg-red-500'
|
||||||
|
default: return 'bg-gray-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCO2BackgroundColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'good': return 'bg-green-50'
|
||||||
|
case 'moderate': return 'bg-yellow-50'
|
||||||
|
case 'poor': return 'bg-orange-50'
|
||||||
|
case 'critical': return 'bg-red-50'
|
||||||
|
default: return 'bg-gray-50'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCO2TextColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'good': return 'text-green-700'
|
||||||
|
case 'moderate': return 'text-yellow-700'
|
||||||
|
case 'poor': return 'text-orange-700'
|
||||||
|
case 'critical': return 'text-red-700'
|
||||||
|
default: return 'text-gray-700'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp * 1000)
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffSecs = Math.floor(diffMs / 1000)
|
||||||
|
|
||||||
|
if (diffSecs < 60) {
|
||||||
|
return `${diffSecs}s ago`
|
||||||
|
} else if (diffSecs < 3600) {
|
||||||
|
return `${Math.floor(diffSecs / 60)}m ago`
|
||||||
|
} else {
|
||||||
|
return date.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -108,15 +108,24 @@ export const useEnergyStore = defineStore('energy', () => {
|
|||||||
if (newDataBuffer.length > 0) {
|
if (newDataBuffer.length > 0) {
|
||||||
const data = newDataBuffer.shift() // Get the oldest data point
|
const data = newDataBuffer.shift() // Get the oldest data point
|
||||||
if (data) {
|
if (data) {
|
||||||
latestMessage.value = data
|
// Handle both legacy and new data formats
|
||||||
|
if (isLegacyData(data)) {
|
||||||
// Update sensor-specific data
|
latestMessage.value = data
|
||||||
updateSensorData(data)
|
updateSensorData(data)
|
||||||
|
|
||||||
// Update time series for chart (aggregate all sensors)
|
// Update time series for chart
|
||||||
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
|
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
|
||||||
timeSeriesData.labels.push(newLabel)
|
timeSeriesData.labels.push(newLabel)
|
||||||
timeSeriesData.datasets[0].data.push(data.value)
|
timeSeriesData.datasets[0].data.push(data.value)
|
||||||
|
} else {
|
||||||
|
// Handle new multi-metric data
|
||||||
|
updateRoomData(data)
|
||||||
|
|
||||||
|
// Update time series for chart (use energy values)
|
||||||
|
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
|
||||||
|
timeSeriesData.labels.push(newLabel)
|
||||||
|
timeSeriesData.datasets[0].data.push(data.energy.value)
|
||||||
|
}
|
||||||
|
|
||||||
if (timeSeriesData.labels.length > MAX_DATA_POINTS) {
|
if (timeSeriesData.labels.length > MAX_DATA_POINTS) {
|
||||||
timeSeriesData.labels.shift()
|
timeSeriesData.labels.shift()
|
||||||
@@ -133,7 +142,11 @@ export const useEnergyStore = defineStore('energy', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSensorData(data: EnergyData) {
|
function isLegacyData(data: any): data is LegacyEnergyData {
|
||||||
|
return 'value' in data && !('energy' in data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSensorData(data: LegacyEnergyData) {
|
||||||
const existingSensor = sensorsData.get(data.sensorId)
|
const existingSensor = sensorsData.get(data.sensorId)
|
||||||
|
|
||||||
if (existingSensor) {
|
if (existingSensor) {
|
||||||
@@ -161,5 +174,74 @@ export const useEnergyStore = defineStore('energy', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isConnected, latestMessage, timeSeriesData, sensorsData, connect, disconnect }
|
function updateRoomData(data: SensorReading) {
|
||||||
|
// Store latest reading
|
||||||
|
latestReadings.set(data.sensorId, data)
|
||||||
|
|
||||||
|
// Get or create room metrics
|
||||||
|
let roomMetrics = roomsData.get(data.room)
|
||||||
|
|
||||||
|
if (!roomMetrics) {
|
||||||
|
roomMetrics = {
|
||||||
|
room: data.room,
|
||||||
|
sensors: [data.sensorId],
|
||||||
|
energy: { current: 0, total: 0, average: 0, unit: data.energy.unit },
|
||||||
|
co2: { current: 0, average: 0, max: 0, status: 'good', unit: data.co2.unit },
|
||||||
|
occupancyEstimate: 'low',
|
||||||
|
lastUpdated: data.timestamp
|
||||||
|
}
|
||||||
|
roomsData.set(data.room, roomMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update room sensors list
|
||||||
|
if (!roomMetrics.sensors.includes(data.sensorId)) {
|
||||||
|
roomMetrics.sensors.push(data.sensorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate room metrics from all sensors in the room
|
||||||
|
const roomSensors = Array.from(latestReadings.values()).filter(reading => reading.room === data.room)
|
||||||
|
|
||||||
|
// Energy calculations
|
||||||
|
roomMetrics.energy.current = roomSensors.reduce((sum, sensor) => sum + sensor.energy.value, 0)
|
||||||
|
roomMetrics.energy.total += data.energy.value // Accumulate total
|
||||||
|
roomMetrics.energy.average = roomMetrics.energy.total / roomSensors.length
|
||||||
|
|
||||||
|
// CO2 calculations
|
||||||
|
const co2Values = roomSensors.map(sensor => sensor.co2.value)
|
||||||
|
roomMetrics.co2.current = co2Values.reduce((sum, val) => sum + val, 0) / co2Values.length
|
||||||
|
roomMetrics.co2.max = Math.max(roomMetrics.co2.max, ...co2Values)
|
||||||
|
roomMetrics.co2.average = (roomMetrics.co2.average + roomMetrics.co2.current) / 2
|
||||||
|
|
||||||
|
// CO2 status classification
|
||||||
|
if (roomMetrics.co2.current < 400) roomMetrics.co2.status = 'good'
|
||||||
|
else if (roomMetrics.co2.current < 1000) roomMetrics.co2.status = 'moderate'
|
||||||
|
else if (roomMetrics.co2.current < 5000) roomMetrics.co2.status = 'poor'
|
||||||
|
else roomMetrics.co2.status = 'critical'
|
||||||
|
|
||||||
|
// Occupancy estimate based on CO2 levels
|
||||||
|
if (roomMetrics.co2.current < 600) roomMetrics.occupancyEstimate = 'low'
|
||||||
|
else if (roomMetrics.co2.current < 1200) roomMetrics.occupancyEstimate = 'medium'
|
||||||
|
else roomMetrics.occupancyEstimate = 'high'
|
||||||
|
|
||||||
|
roomMetrics.lastUpdated = data.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCO2Status(ppm: number): 'good' | 'moderate' | 'poor' | 'critical' {
|
||||||
|
if (ppm < 400) return 'good'
|
||||||
|
if (ppm < 1000) return 'moderate'
|
||||||
|
if (ppm < 5000) return 'poor'
|
||||||
|
return 'critical'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
latestMessage,
|
||||||
|
timeSeriesData,
|
||||||
|
sensorsData,
|
||||||
|
roomsData,
|
||||||
|
latestReadings,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
getCO2Status
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,6 +47,10 @@
|
|||||||
<!-- Charts and Knowledge Cards Row -->
|
<!-- Charts and Knowledge Cards Row -->
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<SensorConsumptionTable />
|
<SensorConsumptionTable />
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<RoomMetricsCard />
|
||||||
|
<AirQualityCard />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -56,6 +60,8 @@ import RealtimeEnergyChartCard from '@/components/cards/RealtimeEnergyChartCard.
|
|||||||
import MetricCard from '@/components/cards/MetricCard.vue'
|
import MetricCard from '@/components/cards/MetricCard.vue'
|
||||||
import GraphMetricCard from '@/components/cards/GraphMetricCard.vue'
|
import GraphMetricCard from '@/components/cards/GraphMetricCard.vue'
|
||||||
import SensorConsumptionTable from '@/components/cards/SensorConsumptionTable.vue'
|
import SensorConsumptionTable from '@/components/cards/SensorConsumptionTable.vue'
|
||||||
|
import RoomMetricsCard from '@/components/cards/RoomMetricsCard.vue'
|
||||||
|
import AirQualityCard from '@/components/cards/AirQualityCard.vue'
|
||||||
import { useEnergyStore } from '@/stores/energy'
|
import { useEnergyStore } from '@/stores/energy'
|
||||||
import { computed, onMounted, onUnmounted } from 'vue'
|
import { computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user