Show real-time room metrics and improve sensor cards

Add a summary of real-time metrics per room, including energy, CO2,
sensor count, and occupancy. Sensor cards now display live readings from
the store instead of mock data. Refactor card logic for reactivity and
update navigation colors for clarity.
This commit is contained in:
rafaeldpsilva
2025-09-03 16:34:04 +01:00
parent eae15a111e
commit 55a2d6d097
6 changed files with 286 additions and 88 deletions

View File

@@ -8,7 +8,7 @@
</div>
<div class="mt-4 sm:mt-0">
<div class="flex items-center gap-2 text-sm text-gray-600">
<div
<div
class="w-3 h-3 rounded-full"
:class="energyStore.isConnected ? 'bg-green-500' : 'bg-red-500'"
></div>
@@ -23,20 +23,14 @@
<div class="flex flex-col lg:flex-row gap-4">
<!-- Filters -->
<div class="flex flex-col sm:flex-row gap-4 flex-1">
<select
v-model="selectedRoom"
class="px-4 py-2 border border-gray-200 rounded-lg bg-white"
>
<select v-model="selectedRoom" class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
<option value="">All Rooms</option>
<option v-for="room in energyStore.availableRooms" :key="room" :value="room">
{{ room }}
</option>
</select>
<select
v-model="selectedType"
class="px-4 py-2 border border-gray-200 rounded-lg bg-white"
>
<select v-model="selectedType" class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
<option value="">All Types</option>
<option value="energy">Energy</option>
<option value="co2">CO2</option>
@@ -46,8 +40,8 @@
<option value="security">Security</option>
</select>
<select
v-model="selectedStatus"
<select
v-model="selectedStatus"
class="px-4 py-2 border border-gray-200 rounded-lg bg-white"
>
<option value="">All Status</option>
@@ -62,13 +56,20 @@
<button
@click="viewMode = 'simple'"
class="px-3 py-1.5 rounded text-sm font-medium transition-colors"
:class="viewMode === 'simple'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'"
:class="
viewMode === 'simple'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
"
>
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 10h16M4 14h16M4 18h16"
/>
</svg>
Simple
</div>
@@ -76,13 +77,20 @@
<button
@click="viewMode = 'detailed'"
class="px-3 py-1.5 rounded text-sm font-medium transition-colors"
:class="viewMode === 'detailed'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'"
:class="
viewMode === 'detailed'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
"
>
<div class="flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
Detailed
</div>
@@ -90,35 +98,78 @@
</div>
</div>
<!-- Real-time Room Metrics Summary -->
<div
v-if="Object.keys(roomMetricsSummary).length > 0"
class="bg-white rounded-xl shadow-sm border border-gray-100 p-4 mb-6"
>
<h2 class="text-lg font-semibold text-gray-900 mb-4">Room-based Real-time Metrics</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div
v-for="(metrics, room) in roomMetricsSummary"
:key="room"
class="bg-gray-50 rounded-lg p-3"
>
<div class="text-sm font-medium text-gray-900 mb-2">{{ room }}</div>
<div class="space-y-1 text-xs">
<div class="flex justify-between">
<span class="text-gray-600">Energy:</span>
<span class="font-medium">{{ metrics.energy.toFixed(2) }} kWh</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">CO2:</span>
<span class="font-medium" :class="getCO2StatusColor(metrics.co2)">
{{ Math.round(metrics.co2) }} ppm
</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Sensors:</span>
<span class="font-medium">{{ metrics.sensorCount }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Occupancy:</span>
<span class="font-medium capitalize" :class="getOccupancyColor(metrics.occupancy)">
{{ metrics.occupancy }}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Sensors Grid -->
<div
<div
class="grid gap-4"
:class="viewMode === 'simple'
? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
: 'grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'"
:class="
viewMode === 'simple'
? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'
: 'grid-cols-1 lg:grid-cols-2 xl:grid-cols-3'
"
>
<!-- Simple Cards -->
<SimpleSensorCard
v-if="viewMode === 'simple'"
v-for="sensor in filteredSensors"
:key="sensor.id"
:sensor="sensor"
:is-executing-action="isExecutingAction"
@execute-action="executeAction"
@show-more="showSensorDetails"
/>
<template v-if="viewMode === 'simple'">
<SimpleSensorCard
v-for="sensor in filteredSensors"
:key="sensor.id"
:sensor="sensor"
:is-executing-action="isExecutingAction"
@execute-action="executeAction"
@show-more="showSensorDetails"
/>
</template>
<!-- Detailed Cards -->
<DetailedSensorCard
v-if="viewMode === 'detailed'"
v-for="sensor in filteredSensors"
:key="sensor.id"
:sensor="sensor"
:available-rooms="energyStore.availableRooms"
:is-executing-action="isExecutingAction"
@update-room="updateRoom"
@execute-action="executeAction"
/>
<template v-else>
<DetailedSensorCard
v-for="sensor in filteredSensors"
:key="sensor.id"
:sensor="sensor"
:available-rooms="energyStore.availableRooms"
:is-executing-action="isExecutingAction"
@update-room="updateRoom"
@execute-action="executeAction"
/>
</template>
</div>
<!-- Empty State -->
@@ -140,7 +191,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useEnergyStore } from '@/stores/energy'
import ActionModal from '@/components/modals/ActionModal.vue'
import SimpleSensorCard from '@/components/cards/SimpleSensorCard.vue'
@@ -163,21 +214,67 @@ const selectedAction = ref<any>(null)
const isExecutingAction = ref(false)
const sensorList = computed(() => {
return Array.from(energyStore.sensorDevices.values()).sort((a, b) =>
a.name.localeCompare(b.name)
)
return Array.from(energyStore.sensorDevices.values()).sort((a, b) => a.name.localeCompare(b.name))
})
const filteredSensors = computed(() => {
return sensorList.value.filter(sensor => {
return sensorList.value.filter((sensor) => {
const matchesRoom = !selectedRoom.value || sensor.room === selectedRoom.value
const matchesType = !selectedType.value || sensor.type === selectedType.value
const matchesStatus = !selectedStatus.value || sensor.status === selectedStatus.value
return matchesRoom && matchesType && matchesStatus
})
})
// Real-time room metrics aggregation
const roomMetricsSummary = computed(() => {
const summary: Record<string, any> = {}
// Process room data from energy store
Array.from(energyStore.roomsData.values()).forEach((roomData) => {
if (roomData.room && roomData.sensors.length > 0) {
summary[roomData.room] = {
energy: roomData.energy.current || 0,
co2: roomData.co2.current || 0,
sensorCount: roomData.sensors.length,
occupancy: roomData.occupancyEstimate || 'low',
lastUpdated: roomData.lastUpdated,
}
}
})
// Fallback: Aggregate from individual sensor readings
if (Object.keys(summary).length === 0) {
const readings = Array.from(energyStore.latestReadings.values())
readings.forEach((reading) => {
if (!reading.room) return
if (!summary[reading.room]) {
summary[reading.room] = {
energy: 0,
co2: 0,
temperature: 0,
sensorCount: 0,
occupancy: 'low',
}
}
summary[reading.room].energy += reading.energy?.value || 0
summary[reading.room].co2 += reading.co2?.value || 0
summary[reading.room].sensorCount += 1
// Simple occupancy estimate based on CO2
const avgCo2 = summary[reading.room].co2 / summary[reading.room].sensorCount
if (avgCo2 > 1200) summary[reading.room].occupancy = 'high'
else if (avgCo2 > 600) summary[reading.room].occupancy = 'medium'
else summary[reading.room].occupancy = 'low'
})
}
return summary
})
const updateRoom = (sensorId: string, newRoom: string) => {
energyStore.updateSensorRoom(sensorId, newRoom)
}
@@ -212,9 +309,41 @@ const closeActionModal = () => {
selectedAction.value = null
}
const showSensorDetails = (sensor: any) => {
const showSensorDetails = () => {
// Switch to detailed view when user wants to see more actions
viewMode.value = 'detailed'
// Optionally scroll to the sensor or highlight it
}
</script>
// Helper functions for styling
const getCO2StatusColor = (co2Level: number) => {
if (co2Level < 400) return 'text-green-600'
if (co2Level < 1000) return 'text-yellow-600'
if (co2Level < 5000) return 'text-orange-600'
return 'text-red-600'
}
const getOccupancyColor = (occupancy: string) => {
switch (occupancy) {
case 'low':
return 'text-green-600'
case 'medium':
return 'text-yellow-600'
case 'high':
return 'text-red-600'
default:
return 'text-gray-600'
}
}
// WebSocket connection for real-time updates
onMounted(() => {
if (!energyStore.isConnected) {
energyStore.connect('ws://192.168.1.73:8000/ws')
}
})
onUnmounted(() => {
// Keep the connection alive for other components
// energyStore.disconnect()
})
</script>