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:
@@ -182,6 +182,8 @@ const getSensorValues = (sensor: any) => {
|
|||||||
|
|
||||||
// Get real-time sensor reading from store
|
// Get real-time sensor reading from store
|
||||||
const latestReading = energyStore.latestReadings.get(sensor.id)
|
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')) {
|
if (sensor.capabilities.monitoring.includes('energy')) {
|
||||||
const energyValue = latestReading?.energy?.value?.toFixed(2) ||
|
const energyValue = latestReading?.energy?.value?.toFixed(2) ||
|
||||||
|
|||||||
@@ -11,13 +11,9 @@
|
|||||||
<p class="text-xs text-gray-500">{{ sensor.room || 'Unassigned' }}</p>
|
<p class="text-xs text-gray-500">{{ sensor.room || 'Unassigned' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Indicator -->
|
<!-- Status Indicator -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<div
|
<div class="w-2 h-2 rounded-full" :class="getSensorStatusColor(sensor.status)"></div>
|
||||||
class="w-2 h-2 rounded-full"
|
|
||||||
:class="getSensorStatusColor(sensor.status)"
|
|
||||||
></div>
|
|
||||||
<span class="text-xs text-gray-500 capitalize">{{ sensor.status }}</span>
|
<span class="text-xs text-gray-500 capitalize">{{ sensor.status }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,8 +21,7 @@
|
|||||||
<!-- Sensor Values -->
|
<!-- Sensor Values -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div v-for="metric in sensorValues" :key="metric.type"
|
<div v-for="metric in sensorValues" :key="metric.type" class="bg-gray-50 rounded p-2">
|
||||||
class="bg-gray-50 rounded p-2">
|
|
||||||
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
|
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
|
||||||
<div class="font-medium text-gray-900">
|
<div class="font-medium text-gray-900">
|
||||||
{{ metric.value }} <span class="text-gray-500">{{ metric.unit }}</span>
|
{{ metric.value }} <span class="text-gray-500">{{ metric.unit }}</span>
|
||||||
@@ -63,15 +58,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No Actions State -->
|
<!-- No Actions State -->
|
||||||
<div v-else class="text-xs text-gray-500 text-center py-2 bg-gray-50 rounded">
|
<div v-else class="text-xs text-gray-500 text-center py-2 bg-gray-50 rounded">Monitor Only</div>
|
||||||
Monitor Only
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useEnergyStore } from '@/stores/energy'
|
import { useEnergyStore } from '@/stores/energy'
|
||||||
|
import { useSensorStore } from '@/stores/sensor'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
sensor: any
|
sensor: any
|
||||||
@@ -84,22 +78,24 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const energyStore = useEnergyStore()
|
const energyStore = useEnergyStore()
|
||||||
|
const sensorStore = useSensorStore()
|
||||||
|
|
||||||
const getSensorValues = (sensor: any) => {
|
const getSensorValues = (sensor: any) => {
|
||||||
const values = []
|
const values = []
|
||||||
|
|
||||||
// Get real-time sensor reading from store
|
const latestReading = energyStore.latestReadings.get(sensor.sensor_id)
|
||||||
const latestReading = energyStore.latestReadings.get(sensor.id)
|
console.log(`Getting values for sensor ${sensor.sensor_id}, found reading:`, latestReading)
|
||||||
|
|
||||||
if (sensor.capabilities.monitoring.includes('energy')) {
|
if (sensor.capabilities.monitoring.includes('energy')) {
|
||||||
const energyValue = latestReading?.energy?.value?.toFixed(2) ||
|
const energyValue =
|
||||||
|
latestReading?.energy?.value?.toFixed(2) ||
|
||||||
energyStore.latestMessage?.value?.toFixed(2) ||
|
energyStore.latestMessage?.value?.toFixed(2) ||
|
||||||
'0.00'
|
'0.00'
|
||||||
values.push({
|
values.push({
|
||||||
type: 'energy',
|
type: 'energy',
|
||||||
label: 'Energy',
|
label: 'Energy',
|
||||||
value: energyValue,
|
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',
|
type: 'co2',
|
||||||
label: 'CO2',
|
label: 'CO2',
|
||||||
value: co2Value,
|
value: co2Value,
|
||||||
unit: latestReading?.co2?.unit || 'ppm'
|
unit: latestReading?.co2?.unit || 'ppm',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sensor.capabilities.monitoring.includes('temperature')) {
|
if (sensor.capabilities.monitoring.includes('temperature')) {
|
||||||
const tempValue = latestReading?.temperature?.value?.toFixed(1) ||
|
const tempValue =
|
||||||
(Math.random() * 8 + 18).toFixed(1)
|
latestReading?.temperature?.value?.toFixed(1) || (Math.random() * 8 + 18).toFixed(1)
|
||||||
values.push({
|
values.push({
|
||||||
type: 'temperature',
|
type: 'temperature',
|
||||||
label: 'Temperature',
|
label: 'Temperature',
|
||||||
value: tempValue,
|
value: tempValue,
|
||||||
unit: latestReading?.temperature?.unit || '°C'
|
unit: latestReading?.temperature?.unit || '°C',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +126,7 @@ const getSensorValues = (sensor: any) => {
|
|||||||
type: 'humidity',
|
type: 'humidity',
|
||||||
label: 'Humidity',
|
label: 'Humidity',
|
||||||
value: Math.floor(Math.random() * 40 + 30),
|
value: Math.floor(Math.random() * 40 + 30),
|
||||||
unit: '%'
|
unit: '%',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +135,7 @@ const getSensorValues = (sensor: any) => {
|
|||||||
type: 'motion',
|
type: 'motion',
|
||||||
label: 'Motion',
|
label: 'Motion',
|
||||||
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
|
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
|
||||||
unit: ''
|
unit: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +145,7 @@ const getSensorValues = (sensor: any) => {
|
|||||||
type: 'status',
|
type: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
value: sensor.status === 'online' ? 'Active' : 'Inactive',
|
value: sensor.status === 'online' ? 'Active' : 'Inactive',
|
||||||
unit: ''
|
unit: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +155,12 @@ const getSensorValues = (sensor: any) => {
|
|||||||
// Reactive sensor values that update automatically
|
// Reactive sensor values that update automatically
|
||||||
const sensorValues = computed(() => getSensorValues(props.sensor))
|
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 getSensorTypeIcon = (type: string) => {
|
||||||
const icons = {
|
const icons = {
|
||||||
energy: '⚡',
|
energy: '⚡',
|
||||||
@@ -167,7 +169,7 @@ const getSensorTypeIcon = (type: string) => {
|
|||||||
humidity: '💧',
|
humidity: '💧',
|
||||||
hvac: '❄️',
|
hvac: '❄️',
|
||||||
lighting: '💡',
|
lighting: '💡',
|
||||||
security: '🔒'
|
security: '🔒',
|
||||||
}
|
}
|
||||||
return icons[type as keyof typeof icons] || '📱'
|
return icons[type as keyof typeof icons] || '📱'
|
||||||
}
|
}
|
||||||
@@ -180,17 +182,21 @@ const getSensorTypeStyle = (type: string) => {
|
|||||||
humidity: { bg: 'bg-blue-100', text: 'text-blue-700' },
|
humidity: { bg: 'bg-blue-100', text: 'text-blue-700' },
|
||||||
hvac: { bg: 'bg-cyan-100', text: 'text-cyan-700' },
|
hvac: { bg: 'bg-cyan-100', text: 'text-cyan-700' },
|
||||||
lighting: { bg: 'bg-amber-100', text: 'text-amber-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' }
|
return styles[type as keyof typeof styles] || { bg: 'bg-gray-100', text: 'text-gray-700' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSensorStatusColor = (status: string) => {
|
const getSensorStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'online': return 'bg-green-500'
|
case 'online':
|
||||||
case 'offline': return 'bg-gray-400'
|
return 'bg-green-500'
|
||||||
case 'error': return 'bg-red-500'
|
case 'offline':
|
||||||
default: return 'bg-gray-400'
|
return 'bg-gray-400'
|
||||||
|
case 'error':
|
||||||
|
return 'bg-red-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-400'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -55,6 +55,12 @@ export const useRoomStore = defineStore('room', () => {
|
|||||||
function updateRoomData(data: SensorReading) {
|
function updateRoomData(data: SensorReading) {
|
||||||
const sensorStore = useSensorStore()
|
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
|
// Store latest reading in sensor store
|
||||||
sensorStore.updateLatestReading(data)
|
sensorStore.updateLatestReading(data)
|
||||||
|
|
||||||
@@ -65,8 +71,8 @@ export const useRoomStore = defineStore('room', () => {
|
|||||||
roomMetrics = {
|
roomMetrics = {
|
||||||
room: data.room,
|
room: data.room,
|
||||||
sensors: [data.sensorId],
|
sensors: [data.sensorId],
|
||||||
energy: { current: 0, total: 0, average: 0, unit: data.energy.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 },
|
co2: { current: 0, average: 0, max: 0, status: 'good', unit: data.co2?.unit || 'ppm' },
|
||||||
occupancyEstimate: 'low',
|
occupancyEstimate: 'low',
|
||||||
lastUpdated: data.timestamp,
|
lastUpdated: data.timestamp,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, reactive } from 'vue'
|
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 {
|
interface SensorReading {
|
||||||
|
id: string
|
||||||
sensorId: string
|
sensorId: string
|
||||||
room: string
|
room: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
@@ -32,6 +39,7 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
|
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
|
||||||
const latestReadings = reactive<Map<string, SensorReading>>(new Map())
|
const latestReadings = reactive<Map<string, SensorReading>>(new Map())
|
||||||
const sensorsData = reactive<Map<string, any>>(new Map()) // Legacy support
|
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 apiLoading = ref(false)
|
||||||
const apiError = ref<string | null>(null)
|
const apiError = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -93,10 +101,30 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
unit: data.unit,
|
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) {
|
function updateLatestReading(reading: SensorReading) {
|
||||||
|
console.log('Updating latest reading for sensor:', reading.sensorId, reading)
|
||||||
|
|
||||||
latestReadings.set(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
|
// API Integration Functions
|
||||||
@@ -104,11 +132,16 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
apiLoading.value = true
|
apiLoading.value = true
|
||||||
apiError.value = null
|
apiError.value = null
|
||||||
|
|
||||||
|
console.log('Making API call...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await apiCall()
|
const result = await apiCall()
|
||||||
|
console.log('API call successful:', result)
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
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')) {
|
if (errorMessage.includes('401') || errorMessage.includes('Authorization')) {
|
||||||
console.warn('Authentication error detected, attempting to re-authenticate...')
|
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
|
apiError.value = errorMessage
|
||||||
console.error('API call failed:', errorMessage)
|
console.error('API call failed:', errorMessage)
|
||||||
return null
|
return null
|
||||||
@@ -147,11 +193,79 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
// Sensors API functions
|
// Sensors API functions
|
||||||
async function fetchApiSensors(params?: { room?: string; sensor_type?: any; status?: any }) {
|
async function fetchApiSensors(params?: { room?: string; sensor_type?: any; status?: any }) {
|
||||||
const result = await handleApiCall(() => sensorsApi.getSensors(params))
|
const result = await handleApiCall(() => sensorsApi.getSensors(params))
|
||||||
if (result && Array.isArray(result)) {
|
|
||||||
result.forEach((sensor) => {
|
if (result) {
|
||||||
sensorDevices.set(sensor.id, sensor)
|
// 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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,161 +293,12 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
return handleApiCall(() => sensorsApi.exportData(params))
|
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 {
|
return {
|
||||||
// State
|
// State
|
||||||
sensorDevices,
|
sensorDevices,
|
||||||
latestReadings,
|
latestReadings,
|
||||||
sensorsData,
|
sensorsData,
|
||||||
|
recentlyUpdatedSensors,
|
||||||
apiLoading,
|
apiLoading,
|
||||||
apiError,
|
apiError,
|
||||||
|
|
||||||
|
|||||||
@@ -72,20 +72,17 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
|||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data)
|
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) {
|
if (data.type === 'proxy_info' && data.sensor_service_url) {
|
||||||
console.log('Received proxy info, reconnecting to sensor service...')
|
console.log('Received proxy info, reconnecting to sensor service...')
|
||||||
// Close current connection gracefully
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.onclose = null // Prevent triggering disconnect handlers
|
socket.onclose = null
|
||||||
socket.close()
|
socket.close()
|
||||||
socket = null
|
socket = null
|
||||||
}
|
}
|
||||||
// Set disconnected state temporarily
|
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
|
|
||||||
// Connect directly to sensor service after a short delay
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('Connecting directly to sensor service at ws://localhost:8007/ws')
|
console.log('Connecting directly to sensor service at ws://localhost:8007/ws')
|
||||||
connect('ws://localhost:8007/ws')
|
connect('ws://localhost:8007/ws')
|
||||||
@@ -113,7 +110,6 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the buffer at intervals
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (newDataBuffer.length > 0) {
|
if (newDataBuffer.length > 0) {
|
||||||
const data = newDataBuffer.shift()
|
const data = newDataBuffer.shift()
|
||||||
@@ -131,19 +127,107 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isLegacyData(data: any): data is LegacyEnergyData {
|
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) {
|
function processIncomingData(data: LegacyEnergyData | SensorReading) {
|
||||||
// Skip non-data messages
|
// Skip non-data messages
|
||||||
if (
|
if ('type' in data && (data.type === 'connection_established' || data.type === 'proxy_info')) {
|
||||||
'type' in data &&
|
|
||||||
(data.type === 'connection_established' || data.type === 'proxy_info')
|
|
||||||
) {
|
|
||||||
console.log('Received system message:', data.type)
|
console.log('Received system message:', data.type)
|
||||||
return
|
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 sensorStore = useSensorStore()
|
||||||
const roomStore = useRoomStore()
|
const roomStore = useRoomStore()
|
||||||
|
|
||||||
@@ -152,18 +236,44 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
|||||||
latestMessage.value = data
|
latestMessage.value = data
|
||||||
sensorStore.updateSensorData(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
|
// 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 {
|
} else {
|
||||||
// Handle new multi-metric data
|
// Handle new multi-metric data
|
||||||
|
// Only update room data if we have the proper structure
|
||||||
|
if (data.energy && data.co2 && data.room) {
|
||||||
roomStore.updateRoomData(data)
|
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()
|
const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString()
|
||||||
timeSeriesData.labels.push(newLabel)
|
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
|
// Keep only the latest data points
|
||||||
|
|||||||
@@ -118,44 +118,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Sensors Grid -->
|
||||||
<div
|
<div
|
||||||
@@ -192,8 +154,28 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</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 -->
|
<!-- 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>
|
<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>
|
<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>
|
<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 showRoomManagementModal = ref(false)
|
||||||
|
|
||||||
const sensorList = computed(() => {
|
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))
|
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) => {
|
const updateRoom = (sensorId: string, newRoom: string) => {
|
||||||
energyStore.updateSensorRoom(sensorId, newRoom)
|
energyStore.updateSensorRoom(sensorId, newRoom)
|
||||||
@@ -342,25 +277,10 @@ const showSensorDetails = () => {
|
|||||||
viewMode.value = 'detailed'
|
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) => {
|
// Reload sensors function
|
||||||
switch (occupancy) {
|
const reloadSensors = async () => {
|
||||||
case 'low':
|
await energyStore.fetchApiSensors()
|
||||||
return 'text-green-600'
|
|
||||||
case 'medium':
|
|
||||||
return 'text-yellow-600'
|
|
||||||
case 'high':
|
|
||||||
return 'text-red-600'
|
|
||||||
default:
|
|
||||||
return 'text-gray-600'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load sensors from API and connect WebSocket for real-time updates
|
// Load sensors from API and connect WebSocket for real-time updates
|
||||||
|
|||||||
Reference in New Issue
Block a user