Improve sensor ID mapping and error handling for real-time data

- Add robust mapping from WebSocket sensor IDs to API sensor IDs -
Enhance error handling for backend connection issues - Remove legacy
room metrics summary from SensorManagementView - Add loading and error
states to sensor grid - Track recently updated sensors for UI feedback -
Normalize incoming sensor data for compatibility
This commit is contained in:
rafaeldpsilva
2025-09-29 13:29:15 +01:00
parent 3299472c85
commit 3681890ec5
6 changed files with 327 additions and 318 deletions

View File

@@ -182,6 +182,8 @@ const getSensorValues = (sensor: any) => {
// Get real-time sensor reading from store
const latestReading = energyStore.latestReadings.get(sensor.id)
console.log(`[Detailed] Getting values for sensor ${sensor.id}, found reading:`, latestReading)
console.log('[Detailed] Available readings:', Array.from(energyStore.latestReadings.keys()))
if (sensor.capabilities.monitoring.includes('energy')) {
const energyValue = latestReading?.energy?.value?.toFixed(2) ||

View File

@@ -11,13 +11,9 @@
<p class="text-xs text-gray-500">{{ sensor.room || 'Unassigned' }}</p>
</div>
</div>
<!-- Status Indicator -->
<div class="flex items-center gap-1">
<div
class="w-2 h-2 rounded-full"
:class="getSensorStatusColor(sensor.status)"
></div>
<div class="w-2 h-2 rounded-full" :class="getSensorStatusColor(sensor.status)"></div>
<span class="text-xs text-gray-500 capitalize">{{ sensor.status }}</span>
</div>
</div>
@@ -25,8 +21,7 @@
<!-- Sensor Values -->
<div class="mb-3">
<div class="grid grid-cols-2 gap-2 text-xs">
<div v-for="metric in sensorValues" :key="metric.type"
class="bg-gray-50 rounded p-2">
<div v-for="metric in sensorValues" :key="metric.type" class="bg-gray-50 rounded p-2">
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
<div class="font-medium text-gray-900">
{{ metric.value }} <span class="text-gray-500">{{ metric.unit }}</span>
@@ -63,15 +58,14 @@
</div>
<!-- No Actions State -->
<div v-else class="text-xs text-gray-500 text-center py-2 bg-gray-50 rounded">
Monitor Only
</div>
<div v-else class="text-xs text-gray-500 text-center py-2 bg-gray-50 rounded">Monitor Only</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useEnergyStore } from '@/stores/energy'
import { useSensorStore } from '@/stores/sensor'
const props = defineProps<{
sensor: any
@@ -84,22 +78,24 @@ const emit = defineEmits<{
}>()
const energyStore = useEnergyStore()
const sensorStore = useSensorStore()
const getSensorValues = (sensor: any) => {
const values = []
// Get real-time sensor reading from store
const latestReading = energyStore.latestReadings.get(sensor.id)
const latestReading = energyStore.latestReadings.get(sensor.sensor_id)
console.log(`Getting values for sensor ${sensor.sensor_id}, found reading:`, latestReading)
if (sensor.capabilities.monitoring.includes('energy')) {
const energyValue = latestReading?.energy?.value?.toFixed(2) ||
energyStore.latestMessage?.value?.toFixed(2) ||
'0.00'
const energyValue =
latestReading?.energy?.value?.toFixed(2) ||
energyStore.latestMessage?.value?.toFixed(2) ||
'0.00'
values.push({
type: 'energy',
label: 'Energy',
value: energyValue,
unit: latestReading?.energy?.unit || energyStore.latestMessage?.unit || 'kWh'
unit: latestReading?.energy?.unit || energyStore.latestMessage?.unit || 'kWh',
})
}
@@ -109,18 +105,18 @@ const getSensorValues = (sensor: any) => {
type: 'co2',
label: 'CO2',
value: co2Value,
unit: latestReading?.co2?.unit || 'ppm'
unit: latestReading?.co2?.unit || 'ppm',
})
}
if (sensor.capabilities.monitoring.includes('temperature')) {
const tempValue = latestReading?.temperature?.value?.toFixed(1) ||
(Math.random() * 8 + 18).toFixed(1)
const tempValue =
latestReading?.temperature?.value?.toFixed(1) || (Math.random() * 8 + 18).toFixed(1)
values.push({
type: 'temperature',
label: 'Temperature',
value: tempValue,
unit: latestReading?.temperature?.unit || '°C'
unit: latestReading?.temperature?.unit || '°C',
})
}
@@ -130,7 +126,7 @@ const getSensorValues = (sensor: any) => {
type: 'humidity',
label: 'Humidity',
value: Math.floor(Math.random() * 40 + 30),
unit: '%'
unit: '%',
})
}
@@ -139,7 +135,7 @@ const getSensorValues = (sensor: any) => {
type: 'motion',
label: 'Motion',
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
unit: ''
unit: '',
})
}
@@ -149,7 +145,7 @@ const getSensorValues = (sensor: any) => {
type: 'status',
label: 'Status',
value: sensor.status === 'online' ? 'Active' : 'Inactive',
unit: ''
unit: '',
})
}
@@ -159,6 +155,12 @@ const getSensorValues = (sensor: any) => {
// Reactive sensor values that update automatically
const sensorValues = computed(() => getSensorValues(props.sensor))
// Check if sensor was recently updated for pulsing animation
const isRecentlyUpdated = computed(() => {
return sensorStore.recentlyUpdatedSensors.has(props.sensor.id) ||
sensorStore.recentlyUpdatedSensors.has(props.sensor.sensor_id)
})
const getSensorTypeIcon = (type: string) => {
const icons = {
energy: '⚡',
@@ -167,7 +169,7 @@ const getSensorTypeIcon = (type: string) => {
humidity: '💧',
hvac: '❄️',
lighting: '💡',
security: '🔒'
security: '🔒',
}
return icons[type as keyof typeof icons] || '📱'
}
@@ -180,17 +182,21 @@ const getSensorTypeStyle = (type: string) => {
humidity: { bg: 'bg-blue-100', text: 'text-blue-700' },
hvac: { bg: 'bg-cyan-100', text: 'text-cyan-700' },
lighting: { bg: 'bg-amber-100', text: 'text-amber-700' },
security: { bg: 'bg-purple-100', text: 'text-purple-700' }
security: { bg: 'bg-purple-100', text: 'text-purple-700' },
}
return styles[type as keyof typeof styles] || { bg: 'bg-gray-100', text: 'text-gray-700' }
}
const getSensorStatusColor = (status: string) => {
switch (status) {
case 'online': return 'bg-green-500'
case 'offline': return 'bg-gray-400'
case 'error': return 'bg-red-500'
default: return 'bg-gray-400'
case 'online':
return 'bg-green-500'
case 'offline':
return 'bg-gray-400'
case 'error':
return 'bg-red-500'
default:
return 'bg-gray-400'
}
}
</script>

View File

@@ -55,6 +55,12 @@ export const useRoomStore = defineStore('room', () => {
function updateRoomData(data: SensorReading) {
const sensorStore = useSensorStore()
// Validate data structure and provide fallbacks
if (!data.energy || !data.co2) {
console.warn('Invalid sensor reading data, missing energy or co2 properties:', data)
return
}
// Store latest reading in sensor store
sensorStore.updateLatestReading(data)
@@ -65,8 +71,8 @@ export const useRoomStore = defineStore('room', () => {
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 },
energy: { current: 0, total: 0, average: 0, unit: data.energy?.unit || 'kWh' },
co2: { current: 0, average: 0, max: 0, status: 'good', unit: data.co2?.unit || 'ppm' },
occupancyEstimate: 'low',
lastUpdated: data.timestamp,
}

View File

@@ -1,8 +1,15 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
import { sensorsApi, SensorType, SensorStatus, type SensorDevice, type SensorAction } from '@/services'
import {
sensorsApi,
SensorType,
SensorStatus,
type SensorDevice,
type SensorAction,
} from '@/services'
interface SensorReading {
id: string
sensorId: string
room: string
timestamp: number
@@ -32,6 +39,7 @@ export const useSensorStore = defineStore('sensor', () => {
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
const latestReadings = reactive<Map<string, SensorReading>>(new Map())
const sensorsData = reactive<Map<string, any>>(new Map()) // Legacy support
const recentlyUpdatedSensors = reactive<Set<string>>(new Set()) // Track recently updated sensors
const apiLoading = ref(false)
const apiError = ref<string | null>(null)
@@ -93,10 +101,30 @@ export const useSensorStore = defineStore('sensor', () => {
unit: data.unit,
})
}
// Mark sensor as recently updated for legacy data as well
recentlyUpdatedSensors.add(data.sensorId)
// Remove from recently updated after 2 seconds
setTimeout(() => {
recentlyUpdatedSensors.delete(data.sensorId)
}, 2000)
}
function updateLatestReading(reading: SensorReading) {
console.log('Updating latest reading for sensor:', reading.sensorId, reading)
latestReadings.set(reading.sensorId, reading)
// Mark sensor as recently updated
recentlyUpdatedSensors.add(reading.sensorId)
// Remove from recently updated after 2 seconds
setTimeout(() => {
recentlyUpdatedSensors.delete(reading.sensorId)
}, 2000)
console.log('Latest readings now contains:', Array.from(latestReadings.keys()))
}
// API Integration Functions
@@ -104,11 +132,16 @@ export const useSensorStore = defineStore('sensor', () => {
apiLoading.value = true
apiError.value = null
console.log('Making API call...')
try {
const result = await apiCall()
console.log('API call successful:', result)
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
console.error('API call error:', error)
console.error('Error message:', errorMessage)
if (errorMessage.includes('401') || errorMessage.includes('Authorization')) {
console.warn('Authentication error detected, attempting to re-authenticate...')
@@ -136,6 +169,19 @@ export const useSensorStore = defineStore('sensor', () => {
}
}
// Check if it's a connection error (backend not running)
if (
errorMessage.includes('fetch') ||
errorMessage.includes('ERR_CONNECTION') ||
errorMessage.includes('ECONNREFUSED')
) {
const backendError =
'Backend server not running on http://localhost:8000. Please start the backend service.'
apiError.value = backendError
console.error('Connection error - backend not running')
return null
}
apiError.value = errorMessage
console.error('API call failed:', errorMessage)
return null
@@ -147,11 +193,79 @@ export const useSensorStore = defineStore('sensor', () => {
// Sensors API functions
async function fetchApiSensors(params?: { room?: string; sensor_type?: any; status?: any }) {
const result = await handleApiCall(() => sensorsApi.getSensors(params))
if (result && Array.isArray(result)) {
result.forEach((sensor) => {
sensorDevices.set(sensor.id, sensor)
})
if (result) {
// Check if result has a sensors property (common API pattern)
if (result.sensors && Array.isArray(result.sensors)) {
result.sensors.forEach((sensor) => {
const sensorKey = sensor.id || sensor._id || sensor.sensor_id
// Normalize sensor data structure for frontend compatibility
const normalizedSensor = {
...sensor,
id: sensorKey,
type: sensor.sensor_type || sensor.type,
capabilities: {
actions: [], // Default empty actions array
monitoring: sensor.capabilities?.monitoring || ['energy'], // Default monitoring capability
...sensor.capabilities,
},
metadata: {
model: sensor.metadata?.model || 'Unknown',
firmware: sensor.metadata?.firmware || 'Unknown',
location: sensor.metadata?.location || sensor.room || 'Unknown',
battery: sensor.metadata?.battery,
signalStrength: sensor.metadata?.signalStrength,
...sensor.metadata,
},
tags: sensor.tags || [],
lastSeen: sensor.last_seen || sensor.lastSeen || Date.now() / 1000,
}
sensorDevices.set(sensorKey, normalizedSensor)
})
}
// Check if result is directly an array
else if (Array.isArray(result)) {
console.log('Result is direct array:', result)
result.forEach((sensor) => {
console.log('Adding sensor:', sensor)
const sensorKey = sensor.id || sensor._id || sensor.sensor_id
// Normalize sensor data structure for frontend compatibility
const normalizedSensor = {
...sensor,
id: sensorKey,
type: sensor.sensor_type || sensor.type,
capabilities: {
actions: [], // Default empty actions array
monitoring: sensor.capabilities?.monitoring || ['energy'], // Default monitoring capability
...sensor.capabilities,
},
metadata: {
model: sensor.metadata?.model || 'Unknown',
firmware: sensor.metadata?.firmware || 'Unknown',
location: sensor.metadata?.location || sensor.room || 'Unknown',
battery: sensor.metadata?.battery,
signalStrength: sensor.metadata?.signalStrength,
...sensor.metadata,
},
tags: sensor.tags || [],
lastSeen: sensor.last_seen || sensor.lastSeen || Date.now() / 1000,
}
sensorDevices.set(sensorKey, normalizedSensor)
})
}
// Log what we actually got
else {
console.log('Unexpected result format:', typeof result, result)
}
} else {
console.log('No result received from API')
}
console.log('Current sensor devices:', Array.from(sensorDevices.entries()))
return result
}
@@ -179,161 +293,12 @@ export const useSensorStore = defineStore('sensor', () => {
return handleApiCall(() => sensorsApi.exportData(params))
}
// Initialize mock sensor devices
function initializeMockSensors() {
const mockSensors: SensorDevice[] = [
{
id: 'sensor_1',
sensor_id: 'sensor_1',
name: 'Energy Monitor 1',
type: 'energy',
sensor_type: 'energy',
room: 'Conference Room A',
status: 'online',
lastSeen: Date.now() / 1000,
total_readings: 1250,
capabilities: {
monitoring: ['energy'],
actions: [],
},
metadata: {
location: 'Wall mounted',
model: 'EM-100',
firmware: '2.1.0',
},
} as SensorDevice,
{
id: 'sensor_2',
sensor_id: 'sensor_2',
name: 'HVAC Controller 1',
type: 'hvac',
sensor_type: 'hvac',
room: 'Conference Room A',
status: 'online',
lastSeen: Date.now() / 1000,
total_readings: 890,
capabilities: {
monitoring: ['temperature', 'co2'],
actions: [
{
id: 'temp_adjust',
name: 'Adjust Temperature',
type: 'adjust',
icon: '🌡️',
parameters: { min: 18, max: 28, step: 0.5 },
},
{
id: 'fan_speed',
name: 'Fan Speed',
type: 'adjust',
icon: '💨',
parameters: { min: 0, max: 5, step: 1 },
},
{ id: 'power_toggle', name: 'Power', type: 'toggle', icon: '⚡' },
],
},
metadata: {
location: 'Ceiling mounted',
model: 'HVAC-200',
firmware: '3.2.1',
},
} as SensorDevice,
{
id: 'sensor_3',
sensor_id: 'sensor_3',
name: 'Smart Light Controller',
type: 'lighting',
sensor_type: 'lighting',
room: 'Office Floor 1',
status: 'online',
lastSeen: Date.now() / 1000,
total_readings: 2100,
capabilities: {
monitoring: ['energy'],
actions: [
{
id: 'brightness',
name: 'Brightness',
type: 'adjust',
icon: '💡',
parameters: { min: 0, max: 100, step: 5 },
},
{ id: 'power_toggle', name: 'Power', type: 'toggle', icon: '⚡' },
{
id: 'scene',
name: 'Scene',
type: 'adjust',
icon: '🎨',
parameters: { options: ['Work', 'Meeting', 'Presentation', 'Relax'] },
},
],
},
metadata: {
location: 'Ceiling grid',
model: 'SL-300',
firmware: '1.5.2',
},
} as SensorDevice,
{
id: 'sensor_4',
sensor_id: 'sensor_4',
name: 'CO2 Sensor',
type: 'co2',
sensor_type: 'co2',
room: 'Meeting Room 1',
status: 'online',
lastSeen: Date.now() / 1000,
total_readings: 1580,
capabilities: {
monitoring: ['co2', 'temperature', 'humidity'],
actions: [{ id: 'calibrate', name: 'Calibrate', type: 'trigger', icon: '⚙️' }],
},
metadata: {
location: 'Wall mounted',
model: 'CO2-150',
firmware: '2.0.3',
battery: 85,
},
} as SensorDevice,
{
id: 'sensor_5',
sensor_id: 'sensor_5',
name: 'Security Camera',
type: 'security',
sensor_type: 'security',
room: 'Lobby',
status: 'online',
lastSeen: Date.now() / 1000,
total_readings: 945,
capabilities: {
monitoring: ['motion'],
actions: [
{ id: 'record_toggle', name: 'Recording', type: 'toggle', icon: '📹' },
{ id: 'ptz_control', name: 'Pan/Tilt/Zoom', type: 'trigger', icon: '🎥' },
{ id: 'night_mode', name: 'Night Mode', type: 'toggle', icon: '🌙' },
],
},
metadata: {
location: 'Corner ceiling',
model: 'SEC-400',
firmware: '4.1.0',
},
} as SensorDevice,
]
mockSensors.forEach((sensor) => {
sensorDevices.set(sensor.id, sensor)
})
}
// Initialize on store creation
initializeMockSensors()
return {
// State
sensorDevices,
latestReadings,
sensorsData,
recentlyUpdatedSensors,
apiLoading,
apiError,

View File

@@ -72,20 +72,17 @@ export const useWebSocketStore = defineStore('websocket', () => {
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
console.log('WebSocket received data:', data)
// Handle proxy info message from API Gateway
if (data.type === 'proxy_info' && data.sensor_service_url) {
console.log('Received proxy info, reconnecting to sensor service...')
// Close current connection gracefully
if (socket) {
socket.onclose = null // Prevent triggering disconnect handlers
socket.onclose = null
socket.close()
socket = null
}
// Set disconnected state temporarily
isConnected.value = false
// Connect directly to sensor service after a short delay
setTimeout(() => {
console.log('Connecting directly to sensor service at ws://localhost:8007/ws')
connect('ws://localhost:8007/ws')
@@ -113,7 +110,6 @@ export const useWebSocketStore = defineStore('websocket', () => {
}
}
// Process the buffer at intervals
setInterval(() => {
if (newDataBuffer.length > 0) {
const data = newDataBuffer.shift()
@@ -131,19 +127,107 @@ export const useWebSocketStore = defineStore('websocket', () => {
}
function isLegacyData(data: any): data is LegacyEnergyData {
return 'value' in data && !('energy' in data)
return 'value' in data && !('energy' in data) && !('co2' in data)
}
function mapWebSocketSensorIdd(webSocketSensorId: string): string {
const sensorStore = useSensorStore()
// First try exact match
if (sensorStore.sensorDevices.has(webSocketSensorId)) {
return webSocketSensorId
}
console.log(`Attempting to map WebSocket sensor ID: ${webSocketSensorId}`)
console.log(
'Available API sensors:',
Array.from(sensorStore.sensorDevices.entries()).map(([id, sensor]) => ({
id,
name: sensor.name,
room: sensor.room,
})),
)
// Try to find a sensor by matching room and type patterns
const sensors = Array.from(sensorStore.sensorDevices.entries())
// Pattern matching for common WebSocket ID formats
for (const [apiSensorId, sensor] of sensors) {
const sensorName = sensor.name.toLowerCase()
const sensorRoom = sensor.room?.toLowerCase() || ''
const wsId = webSocketSensorId.toLowerCase()
console.log(`Checking sensor: ${sensor.name} (${sensorRoom}) against ${webSocketSensorId}`)
// Room-based matching (more comprehensive)
if (wsId.includes('living') && sensorName.includes('living')) return apiSensorId
if (wsId.includes('lr') && sensorName.includes('living')) return apiSensorId
if (wsId.includes('bt') && sensorName.includes('bathroom')) return apiSensorId // bt = bathroom
if (wsId.includes('bathroom') && sensorName.includes('bathroom')) return apiSensorId
if (wsId.includes('br') && sensorName.includes('bedroom')) return apiSensorId // br = bedroom
if (wsId.includes('bedroom') && sensorName.includes('bedroom')) return apiSensorId
if (wsId.includes('kt') && sensorName.includes('kitchen')) return apiSensorId // kt = kitchen
if (wsId.includes('kitchen') && sensorName.includes('kitchen')) return apiSensorId
if (wsId.includes('gr') && sensorName.includes('garage')) return apiSensorId // gr = garage
if (wsId.includes('garage') && sensorName.includes('garage')) return apiSensorId
// Type-based matching
if (wsId.includes('energy') && sensorName.includes('energy')) return apiSensorId
if (wsId.includes('co2') && sensorName.includes('co2')) return apiSensorId
if (wsId.includes('temp') && sensorName.includes('temp')) return apiSensorId
if (wsId.includes('humidity') && sensorName.includes('humidity')) return apiSensorId
// Combined room + type matching for better accuracy
if (
wsId.includes('bathroom') &&
wsId.includes('humidity') &&
sensorName.includes('bathroom') &&
sensorName.includes('humidity')
)
return apiSensorId
if (
wsId.includes('bedroom') &&
wsId.includes('temp') &&
sensorName.includes('bedroom') &&
sensorName.includes('temp')
)
return apiSensorId
if (
wsId.includes('kitchen') &&
wsId.includes('humidity') &&
sensorName.includes('kitchen') &&
sensorName.includes('humidity')
)
return apiSensorId
}
// If no mapping found, let's try to use the first sensor as a fallback for testing
if (sensors.length > 0) {
const fallbackSensor = sensors[0]
console.warn(
`No mapping found for ${webSocketSensorId}, using fallback sensor: ${fallbackSensor[1].name}`,
)
return fallbackSensor[0]
}
console.warn(`Could not map WebSocket sensor ID ${webSocketSensorId} to any API sensor`)
return webSocketSensorId // Return original if no mapping found
}
function processIncomingData(data: LegacyEnergyData | SensorReading) {
// Skip non-data messages
if (
'type' in data &&
(data.type === 'connection_established' || data.type === 'proxy_info')
) {
if ('type' in data && (data.type === 'connection_established' || data.type === 'proxy_info')) {
console.log('Received system message:', data.type)
return
}
console.log('Processing incoming data:', data)
// Normalize property names: sensor_id -> sensorId
if ('sensor_id' in data && !('sensorId' in data)) {
data.sensorId = data.sensor_id
}
const sensorStore = useSensorStore()
const roomStore = useRoomStore()
@@ -152,18 +236,44 @@ export const useWebSocketStore = defineStore('websocket', () => {
latestMessage.value = data
sensorStore.updateSensorData(data)
// Convert legacy data to SensorReading format for individual sensor updates
const mappedSensorId = mapWebSocketSensorId(data.sensorId)
const sensorReading = {
sensorId: mappedSensorId,
room: 'Unknown', // Legacy data doesn't include room info
timestamp: data.timestamp,
energy: {
value: data.value,
unit: data.unit,
},
co2: {
value: 400, // Default CO2 value for legacy data
unit: 'ppm',
},
}
console.log(`Mapped WebSocket sensor ID ${data.sensorId} to ${mappedSensorId}`)
sensorStore.updateLatestReading(sensorReading)
// Update time series for chart
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
timeSeriesData.labels.push(newLabel)
timeSeriesData.datasets[0].data.push(data.value)
} else {
// Handle new multi-metric data
roomStore.updateRoomData(data)
// Only update room data if we have the proper structure
if (data.energy && data.co2 && data.room) {
roomStore.updateRoomData(data)
}
// Update time series for chart (use energy values)
// Map the sensor ID for individual sensor updates
// const mappedSensorId = mapWebSocketSensorId(data.sensorId)
const mappedData = { ...data, sensorId: data.sensorId, id: data.sensorId }
sensorStore.updateLatestReading(mappedData) // Update individual sensor readings for cards
// Update time series for chart (use energy values if available)
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
timeSeriesData.labels.push(newLabel)
timeSeriesData.datasets[0].data.push(data.energy.value)
timeSeriesData.datasets[0].data.push(data.energy?.value || 0)
}
// Keep only the latest data points

View File

@@ -118,44 +118,6 @@
</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
@@ -192,8 +154,28 @@
</template>
</div>
<!-- Loading State -->
<div v-if="energyStore.apiLoading" class="text-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Loading sensors...</h3>
<p class="text-gray-600">Fetching sensor data from the backend</p>
</div>
<!-- Error State -->
<div v-else-if="energyStore.apiError" class="text-center py-12">
<div class="text-red-400 text-6xl mb-4"></div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Error loading sensors</h3>
<p class="text-gray-600 mb-4">{{ energyStore.apiError }}</p>
<button
@click="reloadSensors"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Try Again
</button>
</div>
<!-- Empty State -->
<div v-if="filteredSensors.length === 0" class="text-center py-12">
<div v-else-if="filteredSensors.length === 0" class="text-center py-12">
<div class="text-gray-400 text-6xl mb-4">🔍</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No sensors found</h3>
<p class="text-gray-600">Try adjusting your filters or check if sensors are connected.</p>
@@ -241,7 +223,7 @@ const isExecutingAction = ref(false)
const showRoomManagementModal = ref(false)
const sensorList = computed(() => {
console.log(energyStore.sensorDevices)
console.log('Sensors from store:', energyStore.sensorDevices)
return Array.from(energyStore.sensorDevices.values()).sort((a, b) => a.name.localeCompare(b.name))
})
@@ -255,53 +237,6 @@ const filteredSensors = computed(() => {
})
})
// 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)
@@ -342,25 +277,10 @@ const showSensorDetails = () => {
viewMode.value = 'detailed'
}
// 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'
}
// Reload sensors function
const reloadSensors = async () => {
await energyStore.fetchApiSensors()
}
// Load sensors from API and connect WebSocket for real-time updates