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:
@@ -179,9 +179,11 @@ const energyStore = useEnergyStore()
|
||||
|
||||
const getSensorValues = (sensor: any) => {
|
||||
const values = []
|
||||
|
||||
|
||||
// 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) ||
|
||||
|
||||
@@ -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>
|
||||
@@ -50,7 +45,7 @@
|
||||
<span class="text-xs">{{ action.icon }}</span>
|
||||
<span class="truncate">{{ action.name }}</span>
|
||||
</button>
|
||||
|
||||
|
||||
<!-- Show more actions if there are more than 3 -->
|
||||
<button
|
||||
v-if="sensor.capabilities.actions.length > 3"
|
||||
@@ -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,81 +78,89 @@ 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',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('co2')) {
|
||||
const co2Value = latestReading?.co2?.value || Math.floor(Math.random() * 800 + 350)
|
||||
values.push({
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('humidity')) {
|
||||
// Fallback to mock data for humidity as it's not in current data model
|
||||
values.push({
|
||||
type: 'humidity',
|
||||
label: 'Humidity',
|
||||
value: Math.floor(Math.random() * 40 + 30),
|
||||
unit: '%'
|
||||
unit: '%',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (sensor.capabilities.monitoring.includes('motion')) {
|
||||
values.push({
|
||||
type: 'motion',
|
||||
label: 'Motion',
|
||||
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
|
||||
unit: ''
|
||||
unit: '',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// If no monitoring capabilities, show generic status
|
||||
if (values.length === 0) {
|
||||
values.push({
|
||||
type: 'status',
|
||||
label: 'Status',
|
||||
value: sensor.status === 'online' ? 'Active' : 'Inactive',
|
||||
unit: ''
|
||||
unit: '',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// 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>
|
||||
</script>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -352,4 +317,4 @@ export const useSensorStore = defineStore('sensor', () => {
|
||||
deleteApiSensor,
|
||||
exportApiData,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -183,4 +293,4 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
||||
connect,
|
||||
disconnect,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user