diff --git a/src/services/api.ts b/src/services/api.ts index 9f212e9..1b6db78 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -75,19 +75,6 @@ export interface SensorReading { metadata?: Record } -export interface SensorInfo { - name: string - sensor_id: string - sensor_type: SensorType - room?: string - status: SensorStatus - first_seen: number - last_seen: number - total_readings: number - latest_values?: Record - metadata?: Record -} - export interface RoomInfo { room: string sensor_count: number @@ -188,6 +175,60 @@ export interface SystemEvent { room?: string } +export interface SensorDevice { + id: string + sensor_id: string + name: string + type: SensorType + sensor_type: SensorType + room: string + status: SensorStatus + location?: string + lastSeen: number + total_readings?: number + capabilities: { + monitoring: string[] + actions: SensorAction[] + } + metadata: { + location?: string + model?: string + firmware?: string + battery?: number + created_at?: string + updated_at?: string + manufacturer?: string + } +} + +export interface SensorAction { + id: string + name: string + type: 'toggle' | 'adjust' | 'trigger' + icon: string + parameters?: { + min?: number + max?: number + step?: number + options?: string[] + } +} + +export interface SensorInfo { + id: string + sensor_id: string + name: string + type: SensorType + sensor_type: SensorType + room: string + status: SensorStatus + location?: string + created_at?: string + updated_at?: string + manufacturer?: string + model?: string +} + export enum SensorType { ENERGY = 'energy', CO2 = 'co2', diff --git a/src/services/sensorsApi.ts b/src/services/sensorsApi.ts index 48b5293..5c4371f 100644 --- a/src/services/sensorsApi.ts +++ b/src/services/sensorsApi.ts @@ -1,6 +1,6 @@ import { apiClient, - type SensorInfo, + type SensorDevice, type SensorReading, type DataQuery, type DataResponse, @@ -13,12 +13,12 @@ export const sensorsApi = { room?: string sensor_type?: SensorType status?: SensorStatus - }): Promise { - return apiClient.get('/api/v1/sensors/get', params) + }): Promise { + return apiClient.get('/api/v1/sensors/get', params) }, - async getSensor(sensorId: string): Promise { - return apiClient.get(`/api/v1/sensors/${sensorId}`) + async getSensor(sensorId: string): Promise { + return apiClient.get(`/api/v1/sensors/${sensorId}`) }, async getSensorData( diff --git a/src/stores/analytics.ts b/src/stores/analytics.ts new file mode 100644 index 0000000..45032b3 --- /dev/null +++ b/src/stores/analytics.ts @@ -0,0 +1,151 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { + analyticsApi, + healthApi, + type AnalyticsSummary, + type EnergyTrends, + type RoomComparison, + type SystemStatus, + type HealthCheck, +} from '@/services' + +export const useAnalyticsStore = defineStore('analytics', () => { + // State + const analyticsData = ref<{ + summary: AnalyticsSummary | null + trends: EnergyTrends | null + roomComparison: RoomComparison | null + }>({ + summary: null, + trends: null, + roomComparison: null, + }) + const systemStatus = ref(null) + const healthStatus = ref(null) + const apiLoading = ref(false) + const apiError = ref(null) + + // API Integration Functions + async function handleApiCall(apiCall: () => Promise): Promise { + apiLoading.value = true + apiError.value = null + + try { + const result = await apiCall() + return result + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + + if (errorMessage.includes('401') || errorMessage.includes('Authorization')) { + console.warn('Authentication error detected, attempting to re-authenticate...') + + try { + const authStore = (window as any).__AUTH_STORE__ + if (authStore && typeof authStore.ensureAuthenticated === 'function') { + const authSuccess = await authStore.ensureAuthenticated() + if (authSuccess) { + console.log('Re-authentication successful, retrying API call...') + try { + const retryResult = await apiCall() + return retryResult + } catch (retryError) { + const retryErrorMessage = + retryError instanceof Error ? retryError.message : 'Retry failed' + apiError.value = retryErrorMessage + console.error('API retry failed:', retryErrorMessage) + return null + } + } + } + } catch (authError) { + console.error('Re-authentication failed:', authError) + } + } + + apiError.value = errorMessage + console.error('API call failed:', errorMessage) + return null + } finally { + apiLoading.value = false + } + } + + // Analytics API functions + async function fetchAnalyticsSummary(hours: number = 24) { + const result = await handleApiCall(() => analyticsApi.getAnalyticsSummary(hours)) + if (result) { + analyticsData.value.summary = result + } + return result + } + + async function fetchEnergyTrends(hours: number = 168) { + const result = await handleApiCall(() => analyticsApi.getEnergyTrends(hours)) + if (result) { + analyticsData.value.trends = result + } + return result + } + + async function fetchRoomComparison(hours: number = 24) { + const result = await handleApiCall(() => analyticsApi.getRoomComparison(hours)) + if (result) { + analyticsData.value.roomComparison = result + } + return result + } + + async function fetchSystemEvents(params?: { + severity?: string + event_type?: string + hours?: number + limit?: number + }) { + return handleApiCall(() => analyticsApi.getEvents(params)) + } + + // Health API functions + async function fetchSystemStatus() { + const result = await handleApiCall(() => healthApi.getStatus()) + if (result) { + systemStatus.value = result + } + return result + } + + async function fetchHealthStatus() { + const result = await handleApiCall(() => healthApi.getHealth()) + if (result) { + healthStatus.value = result + } + return result + } + + // Initialize data from APIs + async function initializeAnalyticsFromApi() { + await Promise.allSettled([ + fetchAnalyticsSummary(), + fetchSystemStatus(), + fetchHealthStatus(), + ]) + } + + return { + // State + analyticsData, + systemStatus, + healthStatus, + apiLoading, + apiError, + + // Actions + fetchAnalyticsSummary, + fetchEnergyTrends, + fetchRoomComparison, + fetchSystemEvents, + fetchSystemStatus, + fetchHealthStatus, + initializeAnalyticsFromApi, + } +}) \ No newline at end of file diff --git a/src/stores/energy.ts b/src/stores/energy.ts index 34d5a73..5f4cb3c 100644 --- a/src/stores/energy.ts +++ b/src/stores/energy.ts @@ -1,858 +1,93 @@ import { defineStore } from 'pinia' -import { ref, reactive } from 'vue' -import { - sensorsApi, - roomsApi, - analyticsApi, - healthApi, - type SensorInfo as ApiSensorInfo, - type RoomInfo as ApiRoomInfo, - type AnalyticsSummary, - type EnergyTrends, - type RoomComparison, - type SystemStatus, - type HealthCheck, -} from '@/services' - -const MAX_DATA_POINTS = 100 // Keep the last 100 data points for the chart - -interface SensorReading { - sensorId: string - room: string - timestamp: number - energy: { - value: number - unit: string - } - co2: { - value: number - unit: string - } - temperature?: { - value: number - unit: string - } -} - -interface RoomMetrics { - room: string - sensors: string[] - energy: { - current: number - total: number - average: number - unit: string - } - co2: { - current: number - average: number - max: number - status: 'good' | 'moderate' | 'poor' | 'critical' - unit: string - } - occupancyEstimate: 'low' | 'medium' | 'high' - lastUpdated: number -} - -interface LegacyEnergyData { - sensorId: string - timestamp: number - value: number - unit: string -} - -interface SensorDevice { - id: string - name: string - type: 'energy' | 'co2' | 'temperature' | 'humidity' | 'hvac' | 'lighting' | 'security' - room: string - status: 'online' | 'offline' | 'error' - lastSeen: number - capabilities: { - monitoring: string[] // e.g., ['energy', 'temperature'] - actions: SensorAction[] // Available actions - } - metadata: { - location: string - model?: string - firmware?: string - battery?: number - } -} - -interface SensorAction { - id: string - name: string - type: 'toggle' | 'adjust' | 'trigger' - icon: string - parameters?: { - min?: number - max?: number - step?: number - options?: string[] - } -} +import { computed } from 'vue' +import { useSensorStore } from './sensor' +import { useRoomStore } from './room' +import { useAnalyticsStore } from './analytics' +import { useWebSocketStore } from './websocket' export const useEnergyStore = defineStore('energy', () => { - // State - const isConnected = ref(false) - const latestMessage = ref(null) - const timeSeriesData = reactive<{ - labels: string[] - datasets: { data: number[] }[] - }>({ - labels: [], - datasets: [{ data: [] }], - }) - - const sensorsData = reactive>(new Map()) // Legacy support - const roomsData = reactive>(new Map()) - const latestReadings = reactive>(new Map()) - const sensorDevices = reactive>(new Map()) - const availableRooms = ref([]) - const roomsLoading = ref(false) - const roomsLoaded = ref(false) - - // API integration state - const apiSensors = ref([]) - const apiRooms = ref([]) - const analyticsData = ref<{ - summary: AnalyticsSummary | null - trends: EnergyTrends | null - roomComparison: RoomComparison | null - }>({ - summary: null, - trends: null, - roomComparison: null, - }) - const systemStatus = ref(null) - const healthStatus = ref(null) - const apiLoading = ref(false) - const apiError = ref(null) - - let socket: WebSocket | null = null - const newDataBuffer: (LegacyEnergyData | SensorReading)[] = [] - - // Actions - function connect(url: string) { - if (isConnected.value && socket) { - console.log('Already connected.') - return - } - - // Close any existing connection first - if (socket) { - socket.onclose = null - socket.onerror = null - socket.onmessage = null - socket.close() - socket = null - } - - console.log(`Connecting to WebSocket at ${url}`) - socket = new WebSocket(url) - - socket.onopen = () => { - console.log('WebSocket connection established.') - isConnected.value = true - } - - socket.onmessage = (event) => { - try { - const data = JSON.parse(event.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.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') - }, 100) - return - } - - newDataBuffer.push(data) - } catch (error) { - console.error('Error parsing incoming data:', error) - } - } - - socket.onclose = (event) => { - console.log(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`) - isConnected.value = false - socket = null - } - - socket.onerror = (error) => { - console.error('WebSocket error:', error) - isConnected.value = false - if (socket) { - socket = null - } - } - - // Process the buffer at intervals - setInterval(() => { - if (newDataBuffer.length > 0) { - const data = newDataBuffer.shift() // Get the oldest data point - if (data) { - // Skip non-data messages (connection establishment, proxy info, etc.) - if ( - 'type' in data && - (data.type === 'connection_established' || data.type === 'proxy_info') - ) { - console.log('Received system message:', data.type) - return - } - - // Handle both legacy and new data formats - if (isLegacyData(data)) { - latestMessage.value = data - updateSensorData(data) - - // 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 - updateRoomData(data) - - // Update time series for chart (use energy values) - const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString() - timeSeriesData.labels.push(newLabel) - timeSeriesData.datasets[0].data.push(data.energy.value) - } - - if (timeSeriesData.labels.length > MAX_DATA_POINTS) { - timeSeriesData.labels.shift() - timeSeriesData.datasets[0].data.shift() - } - } - } - }, 500) // Process every 500ms - } - - function disconnect() { - if (socket) { - socket.close() - } - } - - function isLegacyData(data: any): data is LegacyEnergyData { - return 'value' in data && !('energy' in data) - } - - function updateSensorData(data: LegacyEnergyData) { - const existingSensor = sensorsData.get(data.sensorId) - - if (existingSensor) { - // Update existing sensor - const newTotal = existingSensor.totalConsumption + data.value - const dataPoints = Math.floor((data.timestamp - existingSensor.lastUpdated) / 60) + 1 // Rough estimate - - sensorsData.set(data.sensorId, { - ...existingSensor, - latestValue: data.value, - totalConsumption: newTotal, - averageConsumption: newTotal / dataPoints, - lastUpdated: data.timestamp, - }) - } else { - // Create new sensor entry - sensorsData.set(data.sensorId, { - sensorId: data.sensorId, - latestValue: data.value, - totalConsumption: data.value, - averageConsumption: data.value, - lastUpdated: data.timestamp, - unit: data.unit, - }) - } - } - - function updateRoomData(data: SensorReading) { - // Store latest reading - latestReadings.set(data.sensorId, data) - - // Get or create room metrics - let roomMetrics = roomsData.get(data.room) - - if (!roomMetrics) { - roomMetrics = { - room: data.room, - sensors: [data.sensorId], - energy: { current: 0, total: 0, average: 0, unit: data.energy.unit }, - co2: { current: 0, average: 0, max: 0, status: 'good', unit: data.co2.unit }, - occupancyEstimate: 'low', - lastUpdated: data.timestamp, - } - roomsData.set(data.room, roomMetrics) - } - - // Update room sensors list - if (!roomMetrics.sensors.includes(data.sensorId)) { - roomMetrics.sensors.push(data.sensorId) - } - - // Recalculate room metrics from all sensors in the room - const roomSensors = Array.from(latestReadings.values()).filter( - (reading) => reading.room === data.room, - ) - - // Energy calculations - roomMetrics.energy.current = roomSensors.reduce((sum, sensor) => sum + sensor.energy.value, 0) - roomMetrics.energy.total += data.energy.value // Accumulate total - roomMetrics.energy.average = roomMetrics.energy.total / roomSensors.length - - // CO2 calculations - const co2Values = roomSensors.map((sensor) => sensor.co2.value) - roomMetrics.co2.current = co2Values.reduce((sum, val) => sum + val, 0) / co2Values.length - roomMetrics.co2.max = Math.max(roomMetrics.co2.max, ...co2Values) - roomMetrics.co2.average = (roomMetrics.co2.average + roomMetrics.co2.current) / 2 - - // CO2 status classification - if (roomMetrics.co2.current < 400) roomMetrics.co2.status = 'good' - else if (roomMetrics.co2.current < 1000) roomMetrics.co2.status = 'moderate' - else if (roomMetrics.co2.current < 5000) roomMetrics.co2.status = 'poor' - else roomMetrics.co2.status = 'critical' - - // Occupancy estimate based on CO2 levels - if (roomMetrics.co2.current < 600) roomMetrics.occupancyEstimate = 'low' - else if (roomMetrics.co2.current < 1200) roomMetrics.occupancyEstimate = 'medium' - else roomMetrics.occupancyEstimate = 'high' - - roomMetrics.lastUpdated = data.timestamp - } - - function getCO2Status(ppm: number): 'good' | 'moderate' | 'poor' | 'critical' { - if (ppm < 400) return 'good' - if (ppm < 1000) return 'moderate' - if (ppm < 5000) return 'poor' - return 'critical' - } - - // Sensor management functions - function updateSensorRoom(sensorId: string, newRoom: string) { - const sensor = sensorDevices.get(sensorId) - if (sensor) { - sensor.room = newRoom - sensorDevices.set(sensorId, { ...sensor }) - } - } - - async function executeSensorAction(sensorId: string, actionId: string, parameters?: any) { - const sensor = sensorDevices.get(sensorId) - if (!sensor) return false - - const action = sensor.capabilities.actions.find((a) => a.id === actionId) - if (!action) return false - - // Simulate API call to device - console.log(`Executing action ${actionId} on sensor ${sensorId}`, parameters) - - // Here you would make the actual API call to control the device - // For now, we'll simulate a successful action - return new Promise((resolve) => { - setTimeout(() => { - console.log(`Action ${action.name} executed successfully on ${sensor.name}`) - resolve(true) - }, 1000) - }) - } - - function getSensorsByRoom(room: string): SensorDevice[] { - return Array.from(sensorDevices.values()).filter((sensor) => sensor.room === room) - } - - function getSensorsByType(type: SensorDevice['type']): SensorDevice[] { - return Array.from(sensorDevices.values()).filter((sensor) => sensor.type === type) - } - - // Room management functions - const loadRoomsFromAPI = async (): Promise => { - if (roomsLoading.value || roomsLoaded.value) { - return // Already loading or loaded - } - - roomsLoading.value = true - - try { - // Use the API client which handles authentication properly - const data = await roomsApi.getRoomNames() - if (data.rooms && Array.isArray(data.rooms)) { - availableRooms.value = data.rooms.sort() - roomsLoaded.value = true - return - } - - // If no rooms found, use empty list - console.warn('No rooms found in API response, starting with empty list') - availableRooms.value = [] - roomsLoaded.value = true - } catch (error) { - console.error('Error loading rooms:', error) - // Start with empty list on error - availableRooms.value = [] - roomsLoaded.value = true - } finally { - roomsLoading.value = false - } - } - - function addRoom(roomName: string): boolean { - if (!roomName.trim()) return false - - // Check if room already exists - if (availableRooms.value.includes(roomName.trim())) { - return false - } - - // Add room to available rooms list - availableRooms.value.push(roomName.trim()) - availableRooms.value.sort() // Keep rooms sorted alphabetically - - console.log(`Added new room: ${roomName}`) - return true - } - - function removeRoom(roomName: string): boolean { - const index = availableRooms.value.indexOf(roomName) - if (index === -1) return false - - // Check if any sensors are assigned to this room - const sensorsInRoom = Array.from(sensorDevices.values()).filter( - (sensor) => sensor.room === roomName, - ) - if (sensorsInRoom.length > 0) { - // Reassign sensors to 'Unassigned' - sensorsInRoom.forEach((sensor) => { - sensor.room = '' - sensorDevices.set(sensor.id, { ...sensor }) - }) - } - - // Remove room data - roomsData.delete(roomName) - - // Remove from available rooms - availableRooms.value.splice(index, 1) - - console.log(`Removed room: ${roomName}`) - return true - } - - function getRoomStats(roomName: string) { - const sensorsInRoom = getSensorsByRoom(roomName) - const roomMetrics = roomsData.get(roomName) - - return { - sensorCount: sensorsInRoom.length, - sensorTypes: [...new Set(sensorsInRoom.map((s) => s.type))], - hasMetrics: !!roomMetrics, - energyConsumption: roomMetrics?.energy.current || 0, - co2Level: roomMetrics?.co2.current || 0, - lastUpdated: roomMetrics?.lastUpdated || null, - } - } - - function getAllRoomsWithStats() { - return availableRooms.value.map((room) => ({ - name: room, - ...getRoomStats(room), - })) - } - - // API Integration Functions - async function handleApiCall(apiCall: () => Promise): Promise { - apiLoading.value = true - apiError.value = null - - try { - const result = await apiCall() - return result - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' - - if (errorMessage.includes('401') || errorMessage.includes('Authorization')) { - console.warn('Authentication error detected, attempting to re-authenticate...') - - // Try to get fresh auth token - try { - const authStore = (window as any).__AUTH_STORE__ - if (authStore && typeof authStore.ensureAuthenticated === 'function') { - const authSuccess = await authStore.ensureAuthenticated() - if (authSuccess) { - console.log('Re-authentication successful, retrying API call...') - // Retry the original API call - try { - const retryResult = await apiCall() - return retryResult - } catch (retryError) { - const retryErrorMessage = - retryError instanceof Error ? retryError.message : 'Retry failed' - apiError.value = retryErrorMessage - console.error('API retry failed:', retryErrorMessage) - return null - } - } - } - } catch (authError) { - console.error('Re-authentication failed:', authError) - } - } - - apiError.value = errorMessage - console.error('API call failed:', errorMessage) - return null - } finally { - apiLoading.value = false - } - } - - // Helper function to transform API sensor to expected format - function transformApiSensor(apiSensor: any) { - return { - id: apiSensor.sensor_id, - sensor_id: apiSensor.sensor_id, - name: apiSensor.name || apiSensor.sensor_id, - type: apiSensor.sensor_type, - sensor_type: apiSensor.sensor_type, - room: apiSensor.room, - status: apiSensor.status === 'active' ? 'online' : apiSensor.status, - location: apiSensor.location, - // Add capabilities based on sensor type - capabilities: { - monitoring: [apiSensor.sensor_type], - actions: [], // API sensors don't have actions yet - }, - // Add metadata - metadata: { - created_at: apiSensor.created_at, - updated_at: apiSensor.updated_at, - manufacturer: apiSensor.manufacturer, - model: apiSensor.model, - }, - } - } - - // Sensors API functions - async function fetchApiSensors(params?: { room?: string; sensor_type?: any; status?: any }) { - const result = await handleApiCall(() => sensorsApi.getSensors(params)) - if (result.sensors) { - result.sensors.forEach((sensor) => { - sensorDevices.set(sensor.id, sensor) - }) - } - return result - } - - async function fetchApiSensorData( - sensorId: string, - params?: { start_time?: number; end_time?: number; limit?: number; offset?: number }, - ) { - return handleApiCall(() => sensorsApi.getSensorData(sensorId, params)) - } - - async function updateApiSensorMetadata(sensorId: string, metadata: Record) { - return handleApiCall(() => sensorsApi.updateSensorMetadata(sensorId, metadata)) - } - - async function deleteApiSensor(sensorId: string) { - return handleApiCall(() => sensorsApi.deleteSensor(sensorId)) - } - - async function exportApiData(params: { - start_time: number - end_time: number - sensor_ids?: string - format?: 'json' | 'csv' - }) { - return handleApiCall(() => sensorsApi.exportData(params)) - } - - // Rooms API functions - async function fetchApiRooms() { - const result = await handleApiCall(() => roomsApi.getRooms()) - if (result) { - apiRooms.value = result - // Update available rooms from API data - const roomNames = result.map((room) => room.room).filter((name) => name) - if (roomNames.length > 0) { - availableRooms.value = [...new Set([...availableRooms.value, ...roomNames])].sort() - } - } - return result - } - - async function fetchApiRoomData( - roomName: string, - params?: { start_time?: number; end_time?: number; limit?: number }, - ) { - return handleApiCall(() => roomsApi.getRoomData(roomName, params)) - } - - // Analytics API functions - async function fetchAnalyticsSummary(hours: number = 24) { - const result = await handleApiCall(() => analyticsApi.getAnalyticsSummary(hours)) - if (result) { - analyticsData.value.summary = result - } - return result - } - - async function fetchEnergyTrends(hours: number = 168) { - const result = await handleApiCall(() => analyticsApi.getEnergyTrends(hours)) - if (result) { - analyticsData.value.trends = result - } - return result - } - - async function fetchRoomComparison(hours: number = 24) { - const result = await handleApiCall(() => analyticsApi.getRoomComparison(hours)) - if (result) { - analyticsData.value.roomComparison = result - } - return result - } - - async function fetchSystemEvents(params?: { - severity?: string - event_type?: string - hours?: number - limit?: number - }) { - return handleApiCall(() => analyticsApi.getEvents(params)) - } - - // Health API functions - async function fetchSystemStatus() { - const result = await handleApiCall(() => healthApi.getStatus()) - if (result) { - systemStatus.value = result - } - return result - } - - async function fetchHealthStatus() { - const result = await handleApiCall(() => healthApi.getHealth()) - if (result) { - healthStatus.value = result - } - return result - } + // Get instances of other stores + const sensorStore = useSensorStore() + const roomStore = useRoomStore() + const analyticsStore = useAnalyticsStore() + const webSocketStore = useWebSocketStore() + + // Delegate to WebSocket store + const connect = (url: string) => webSocketStore.connect(url) + const disconnect = () => webSocketStore.disconnect() // Initialize data from APIs async function initializeFromApi() { await Promise.allSettled([ - loadRoomsFromAPI(), // Load room names first - fetchApiSensors(), - fetchApiRooms(), - fetchAnalyticsSummary(), - fetchSystemStatus(), - fetchHealthStatus(), + roomStore.loadRoomsFromAPI(), + sensorStore.fetchApiSensors(), + roomStore.fetchApiRooms(), + analyticsStore.initializeAnalyticsFromApi(), ]) } - // Initialize mock sensors on store creation - - // Initialize mock sensor devices - function initializeMockSensors() { - const mockSensors: SensorDevice[] = [ - { - id: 'sensor_1', - name: 'Energy Monitor 1', - type: 'energy', - room: 'Conference Room A', - status: 'online', - lastSeen: Date.now() / 1000, - capabilities: { - monitoring: ['energy'], - actions: [], - }, - metadata: { - location: 'Wall mounted', - model: 'EM-100', - firmware: '2.1.0', - }, - }, - { - id: 'sensor_2', - name: 'HVAC Controller 1', - type: 'hvac', - room: 'Conference Room A', - status: 'online', - lastSeen: Date.now() / 1000, - 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', - }, - }, - { - id: 'sensor_3', - name: 'Smart Light Controller', - type: 'lighting', - room: 'Office Floor 1', - status: 'online', - lastSeen: Date.now() / 1000, - 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', - }, - }, - { - id: 'sensor_4', - name: 'CO2 Sensor', - type: 'co2', - room: 'Meeting Room 1', - status: 'online', - lastSeen: Date.now() / 1000, - 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, - }, - }, - { - id: 'sensor_5', - name: 'Security Camera', - type: 'security', - room: 'Lobby', - status: 'online', - lastSeen: Date.now() / 1000, - 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', - }, - }, - ] - - mockSensors.forEach((sensor) => { - sensorDevices.set(sensor.id, sensor) - }) - } - //initializeMockSensors() - - // Load rooms from API on store initialization - loadRoomsFromAPI() - return { - // WebSocket state - isConnected, - latestMessage, - timeSeriesData, - sensorsData, - roomsData, - latestReadings, - sensorDevices, - availableRooms, - roomsLoading, - roomsLoaded, + // WebSocket state (delegated) + isConnected: computed(() => webSocketStore.isConnected), + latestMessage: computed(() => webSocketStore.latestMessage), + timeSeriesData: computed(() => webSocketStore.timeSeriesData), - // API state - apiSensors, - apiRooms, - analyticsData, - systemStatus, - healthStatus, - apiLoading, - apiError, + // Sensor state (delegated) + sensorsData: computed(() => sensorStore.sensorsData), + sensorDevices: computed(() => sensorStore.sensorDevices), + latestReadings: computed(() => sensorStore.latestReadings), + apiSensors: computed(() => Array.from(sensorStore.sensorDevices.values())), // Convert Map to Array - // WebSocket functions + // Room state (delegated) + roomsData: computed(() => roomStore.roomsData), + availableRooms: computed(() => roomStore.availableRooms), + roomsLoading: computed(() => roomStore.roomsLoading), + roomsLoaded: computed(() => roomStore.roomsLoaded), + apiRooms: computed(() => roomStore.apiRooms), + + // Analytics state (delegated) + analyticsData: computed(() => analyticsStore.analyticsData), + systemStatus: computed(() => analyticsStore.systemStatus), + healthStatus: computed(() => analyticsStore.healthStatus), + + // Combined API loading/error state + apiLoading: computed(() => sensorStore.apiLoading || roomStore.apiLoading || analyticsStore.apiLoading), + apiError: computed(() => sensorStore.apiError || roomStore.apiError || analyticsStore.apiError), + + // WebSocket functions (delegated) connect, disconnect, - getCO2Status, - updateSensorRoom, - executeSensorAction, - getSensorsByRoom, - getSensorsByType, - loadRoomsFromAPI, - addRoom, - removeRoom, - getRoomStats, - getAllRoomsWithStats, - // API functions - fetchApiSensors, - fetchApiSensorData, - updateApiSensorMetadata, - deleteApiSensor, - exportApiData, - fetchApiRooms, - fetchApiRoomData, - fetchAnalyticsSummary, - fetchEnergyTrends, - fetchRoomComparison, - fetchSystemEvents, - fetchSystemStatus, - fetchHealthStatus, + // Sensor functions (delegated) + updateSensorRoom: sensorStore.updateSensorRoom, + executeSensorAction: sensorStore.executeSensorAction, + getSensorsByRoom: sensorStore.getSensorsByRoom, + getSensorsByType: sensorStore.getSensorsByType, + fetchApiSensors: sensorStore.fetchApiSensors, + fetchApiSensorData: sensorStore.fetchApiSensorData, + updateApiSensorMetadata: sensorStore.updateApiSensorMetadata, + deleteApiSensor: sensorStore.deleteApiSensor, + exportApiData: sensorStore.exportApiData, + + // Room functions (delegated) + getCO2Status: roomStore.getCO2Status, + loadRoomsFromAPI: roomStore.loadRoomsFromAPI, + addRoom: roomStore.addRoom, + removeRoom: roomStore.removeRoom, + getRoomStats: roomStore.getRoomStats, + getAllRoomsWithStats: roomStore.getAllRoomsWithStats, + fetchApiRooms: roomStore.fetchApiRooms, + fetchApiRoomData: roomStore.fetchApiRoomData, + + // Analytics functions (delegated) + fetchAnalyticsSummary: analyticsStore.fetchAnalyticsSummary, + fetchEnergyTrends: analyticsStore.fetchEnergyTrends, + fetchRoomComparison: analyticsStore.fetchRoomComparison, + fetchSystemEvents: analyticsStore.fetchSystemEvents, + fetchSystemStatus: analyticsStore.fetchSystemStatus, + fetchHealthStatus: analyticsStore.fetchHealthStatus, + + // Initialize function initializeFromApi, } }) diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..ef316f7 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,16 @@ +// Store exports +export { useEnergyStore } from './energy' +export { useSensorStore } from './sensor' +export { useRoomStore } from './room' +export { useAnalyticsStore } from './analytics' +export { useWebSocketStore } from './websocket' + +// Re-export types from services for convenience +export type { + RoomInfo as ApiRoomInfo, + AnalyticsSummary, + EnergyTrends, + RoomComparison, + SystemStatus, + HealthCheck, +} from '@/services' \ No newline at end of file diff --git a/src/stores/room.ts b/src/stores/room.ts new file mode 100644 index 0000000..ef082cf --- /dev/null +++ b/src/stores/room.ts @@ -0,0 +1,298 @@ +import { defineStore } from 'pinia' +import { ref, reactive } from 'vue' +import { roomsApi, type RoomInfo as ApiRoomInfo } from '@/services' +import { useSensorStore } from './sensor' + +interface RoomMetrics { + room: string + sensors: string[] + energy: { + current: number + total: number + average: number + unit: string + } + co2: { + current: number + average: number + max: number + status: 'good' | 'moderate' | 'poor' | 'critical' + unit: string + } + occupancyEstimate: 'low' | 'medium' | 'high' + lastUpdated: number +} + +interface SensorReading { + sensorId: string + room: string + timestamp: number + energy: { + value: number + unit: string + } + co2: { + value: number + unit: string + } + temperature?: { + value: number + unit: string + } +} + +export const useRoomStore = defineStore('room', () => { + // State + const roomsData = reactive>(new Map()) + const availableRooms = ref([]) + const apiRooms = ref([]) + const roomsLoading = ref(false) + const roomsLoaded = ref(false) + const apiLoading = ref(false) + const apiError = ref(null) + + // Actions + function updateRoomData(data: SensorReading) { + const sensorStore = useSensorStore() + + // Store latest reading in sensor store + sensorStore.updateLatestReading(data) + + // Get or create room metrics + let roomMetrics = roomsData.get(data.room) + + if (!roomMetrics) { + roomMetrics = { + room: data.room, + sensors: [data.sensorId], + energy: { current: 0, total: 0, average: 0, unit: data.energy.unit }, + co2: { current: 0, average: 0, max: 0, status: 'good', unit: data.co2.unit }, + occupancyEstimate: 'low', + lastUpdated: data.timestamp, + } + roomsData.set(data.room, roomMetrics) + } + + // Update room sensors list + if (!roomMetrics.sensors.includes(data.sensorId)) { + roomMetrics.sensors.push(data.sensorId) + } + + // Recalculate room metrics from all sensors in the room + const roomSensors = Array.from(sensorStore.latestReadings.values()).filter( + (reading) => reading.room === data.room, + ) + + // Energy calculations + roomMetrics.energy.current = roomSensors.reduce((sum, sensor) => sum + sensor.energy.value, 0) + roomMetrics.energy.total += data.energy.value // Accumulate total + roomMetrics.energy.average = roomMetrics.energy.total / roomSensors.length + + // CO2 calculations + const co2Values = roomSensors.map((sensor) => sensor.co2.value) + roomMetrics.co2.current = co2Values.reduce((sum, val) => sum + val, 0) / co2Values.length + roomMetrics.co2.max = Math.max(roomMetrics.co2.max, ...co2Values) + roomMetrics.co2.average = (roomMetrics.co2.average + roomMetrics.co2.current) / 2 + + // CO2 status classification + if (roomMetrics.co2.current < 400) roomMetrics.co2.status = 'good' + else if (roomMetrics.co2.current < 1000) roomMetrics.co2.status = 'moderate' + else if (roomMetrics.co2.current < 5000) roomMetrics.co2.status = 'poor' + else roomMetrics.co2.status = 'critical' + + // Occupancy estimate based on CO2 levels + if (roomMetrics.co2.current < 600) roomMetrics.occupancyEstimate = 'low' + else if (roomMetrics.co2.current < 1200) roomMetrics.occupancyEstimate = 'medium' + else roomMetrics.occupancyEstimate = 'high' + + roomMetrics.lastUpdated = data.timestamp + } + + function getCO2Status(ppm: number): 'good' | 'moderate' | 'poor' | 'critical' { + if (ppm < 400) return 'good' + if (ppm < 1000) return 'moderate' + if (ppm < 5000) return 'poor' + return 'critical' + } + + const loadRoomsFromAPI = async (): Promise => { + if (roomsLoading.value || roomsLoaded.value) { + return + } + + roomsLoading.value = true + + try { + const data = await roomsApi.getRoomNames() + if (data.rooms && Array.isArray(data.rooms)) { + availableRooms.value = data.rooms.sort() + roomsLoaded.value = true + return + } + + console.warn('No rooms found in API response, starting with empty list') + availableRooms.value = [] + roomsLoaded.value = true + } catch (error) { + console.error('Error loading rooms:', error) + availableRooms.value = [] + roomsLoaded.value = true + } finally { + roomsLoading.value = false + } + } + + function addRoom(roomName: string): boolean { + if (!roomName.trim()) return false + + if (availableRooms.value.includes(roomName.trim())) { + return false + } + + availableRooms.value.push(roomName.trim()) + availableRooms.value.sort() + + console.log(`Added new room: ${roomName}`) + return true + } + + function removeRoom(roomName: string): boolean { + const sensorStore = useSensorStore() + const index = availableRooms.value.indexOf(roomName) + if (index === -1) return false + + // Check if any sensors are assigned to this room + const sensorsInRoom = sensorStore.getSensorsByRoom(roomName) + if (sensorsInRoom.length > 0) { + // Reassign sensors to empty room + sensorsInRoom.forEach((sensor) => { + sensor.room = '' + sensorStore.sensorDevices.set(sensor.id, { ...sensor }) + }) + } + + // Remove room data + roomsData.delete(roomName) + + // Remove from available rooms + availableRooms.value.splice(index, 1) + + console.log(`Removed room: ${roomName}`) + return true + } + + function getRoomStats(roomName: string) { + const sensorStore = useSensorStore() + const sensorsInRoom = sensorStore.getSensorsByRoom(roomName) + const roomMetrics = roomsData.get(roomName) + + return { + sensorCount: sensorsInRoom.length, + sensorTypes: [...new Set(sensorsInRoom.map((s) => s.type))], + hasMetrics: !!roomMetrics, + energyConsumption: roomMetrics?.energy.current || 0, + co2Level: roomMetrics?.co2.current || 0, + lastUpdated: roomMetrics?.lastUpdated || null, + } + } + + function getAllRoomsWithStats() { + return availableRooms.value.map((room) => ({ + name: room, + ...getRoomStats(room), + })) + } + + // API Integration Functions + async function handleApiCall(apiCall: () => Promise): Promise { + apiLoading.value = true + apiError.value = null + + try { + const result = await apiCall() + return result + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + + if (errorMessage.includes('401') || errorMessage.includes('Authorization')) { + console.warn('Authentication error detected, attempting to re-authenticate...') + + try { + const authStore = (window as any).__AUTH_STORE__ + if (authStore && typeof authStore.ensureAuthenticated === 'function') { + const authSuccess = await authStore.ensureAuthenticated() + if (authSuccess) { + console.log('Re-authentication successful, retrying API call...') + try { + const retryResult = await apiCall() + return retryResult + } catch (retryError) { + const retryErrorMessage = + retryError instanceof Error ? retryError.message : 'Retry failed' + apiError.value = retryErrorMessage + console.error('API retry failed:', retryErrorMessage) + return null + } + } + } + } catch (authError) { + console.error('Re-authentication failed:', authError) + } + } + + apiError.value = errorMessage + console.error('API call failed:', errorMessage) + return null + } finally { + apiLoading.value = false + } + } + + // Rooms API functions + async function fetchApiRooms() { + const result = await handleApiCall(() => roomsApi.getRooms()) + if (result) { + apiRooms.value = result + // Update available rooms from API data + const roomNames = result.map((room) => room.room).filter((name) => name) + if (roomNames.length > 0) { + availableRooms.value = [...new Set([...availableRooms.value, ...roomNames])].sort() + } + } + return result + } + + async function fetchApiRoomData( + roomName: string, + params?: { start_time?: number; end_time?: number; limit?: number }, + ) { + return handleApiCall(() => roomsApi.getRoomData(roomName, params)) + } + + // Initialize rooms on store creation + loadRoomsFromAPI() + + return { + // State + roomsData, + availableRooms, + apiRooms, + roomsLoading, + roomsLoaded, + apiLoading, + apiError, + + // Actions + updateRoomData, + getCO2Status, + loadRoomsFromAPI, + addRoom, + removeRoom, + getRoomStats, + getAllRoomsWithStats, + + // API functions + fetchApiRooms, + fetchApiRoomData, + } +}) \ No newline at end of file diff --git a/src/stores/sensor.ts b/src/stores/sensor.ts new file mode 100644 index 0000000..82f1b19 --- /dev/null +++ b/src/stores/sensor.ts @@ -0,0 +1,355 @@ +import { defineStore } from 'pinia' +import { ref, reactive } from 'vue' +import { sensorsApi, SensorType, SensorStatus, type SensorDevice, type SensorAction } from '@/services' + +interface SensorReading { + sensorId: string + room: string + timestamp: number + energy: { + value: number + unit: string + } + co2: { + value: number + unit: string + } + temperature?: { + value: number + unit: string + } +} + +interface LegacyEnergyData { + sensorId: string + timestamp: number + value: number + unit: string +} + +export const useSensorStore = defineStore('sensor', () => { + // State + const sensorDevices = reactive>(new Map()) + const latestReadings = reactive>(new Map()) + const sensorsData = reactive>(new Map()) // Legacy support + const apiLoading = ref(false) + const apiError = ref(null) + + // Actions + function updateSensorRoom(sensorId: string, newRoom: string) { + const sensor = sensorDevices.get(sensorId) + if (sensor) { + sensor.room = newRoom + sensorDevices.set(sensorId, { ...sensor }) + } + } + + async function executeSensorAction(sensorId: string, actionId: string, parameters?: any) { + const sensor = sensorDevices.get(sensorId) + if (!sensor) return false + + const action = sensor.capabilities.actions.find((a) => a.id === actionId) + if (!action) return false + + console.log(`Executing action ${actionId} on sensor ${sensorId}`, parameters) + + return new Promise((resolve) => { + setTimeout(() => { + console.log(`Action ${action.name} executed successfully on ${sensor.name}`) + resolve(true) + }, 1000) + }) + } + + function getSensorsByRoom(room: string): SensorDevice[] { + return Array.from(sensorDevices.values()).filter((sensor) => sensor.room === room) + } + + function getSensorsByType(type: SensorDevice['type']): SensorDevice[] { + return Array.from(sensorDevices.values()).filter((sensor) => sensor.type === type) + } + + function updateSensorData(data: LegacyEnergyData) { + const existingSensor = sensorsData.get(data.sensorId) + + if (existingSensor) { + const newTotal = existingSensor.totalConsumption + data.value + const dataPoints = Math.floor((data.timestamp - existingSensor.lastUpdated) / 60) + 1 + + sensorsData.set(data.sensorId, { + ...existingSensor, + latestValue: data.value, + totalConsumption: newTotal, + averageConsumption: newTotal / dataPoints, + lastUpdated: data.timestamp, + }) + } else { + sensorsData.set(data.sensorId, { + sensorId: data.sensorId, + latestValue: data.value, + totalConsumption: data.value, + averageConsumption: data.value, + lastUpdated: data.timestamp, + unit: data.unit, + }) + } + } + + function updateLatestReading(reading: SensorReading) { + latestReadings.set(reading.sensorId, reading) + } + + // API Integration Functions + async function handleApiCall(apiCall: () => Promise): Promise { + apiLoading.value = true + apiError.value = null + + try { + const result = await apiCall() + return result + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + + if (errorMessage.includes('401') || errorMessage.includes('Authorization')) { + console.warn('Authentication error detected, attempting to re-authenticate...') + + try { + const authStore = (window as any).__AUTH_STORE__ + if (authStore && typeof authStore.ensureAuthenticated === 'function') { + const authSuccess = await authStore.ensureAuthenticated() + if (authSuccess) { + console.log('Re-authentication successful, retrying API call...') + try { + const retryResult = await apiCall() + return retryResult + } catch (retryError) { + const retryErrorMessage = + retryError instanceof Error ? retryError.message : 'Retry failed' + apiError.value = retryErrorMessage + console.error('API retry failed:', retryErrorMessage) + return null + } + } + } + } catch (authError) { + console.error('Re-authentication failed:', authError) + } + } + + apiError.value = errorMessage + console.error('API call failed:', errorMessage) + return null + } finally { + apiLoading.value = false + } + } + + // 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) + }) + } + return result + } + + async function fetchApiSensorData( + sensorId: string, + params?: { start_time?: number; end_time?: number; limit?: number; offset?: number }, + ) { + return handleApiCall(() => sensorsApi.getSensorData(sensorId, params)) + } + + async function updateApiSensorMetadata(sensorId: string, metadata: Record) { + return handleApiCall(() => sensorsApi.updateSensorMetadata(sensorId, metadata)) + } + + async function deleteApiSensor(sensorId: string) { + return handleApiCall(() => sensorsApi.deleteSensor(sensorId)) + } + + async function exportApiData(params: { + start_time: number + end_time: number + sensor_ids?: string + format?: 'json' | 'csv' + }) { + 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, + apiLoading, + apiError, + + // Actions + updateSensorRoom, + executeSensorAction, + getSensorsByRoom, + getSensorsByType, + updateSensorData, + updateLatestReading, + + // API functions + fetchApiSensors, + fetchApiSensorData, + updateApiSensorMetadata, + deleteApiSensor, + exportApiData, + } +}) \ No newline at end of file diff --git a/src/stores/websocket.ts b/src/stores/websocket.ts new file mode 100644 index 0000000..37c2bc1 --- /dev/null +++ b/src/stores/websocket.ts @@ -0,0 +1,186 @@ +import { defineStore } from 'pinia' +import { ref, reactive } from 'vue' +import { useSensorStore } from './sensor' +import { useRoomStore } from './room' + +const MAX_DATA_POINTS = 100 + +interface LegacyEnergyData { + sensorId: string + timestamp: number + value: number + unit: string +} + +interface SensorReading { + sensorId: string + room: string + timestamp: number + energy: { + value: number + unit: string + } + co2: { + value: number + unit: string + } + temperature?: { + value: number + unit: string + } +} + +export const useWebSocketStore = defineStore('websocket', () => { + // State + const isConnected = ref(false) + const latestMessage = ref(null) + const timeSeriesData = reactive<{ + labels: string[] + datasets: { data: number[] }[] + }>({ + labels: [], + datasets: [{ data: [] }], + }) + + let socket: WebSocket | null = null + const newDataBuffer: (LegacyEnergyData | SensorReading)[] = [] + + // Actions + function connect(url: string) { + if (isConnected.value && socket) { + console.log('Already connected.') + return + } + + // Close any existing connection first + if (socket) { + socket.onclose = null + socket.onerror = null + socket.onmessage = null + socket.close() + socket = null + } + + console.log(`Connecting to WebSocket at ${url}`) + socket = new WebSocket(url) + + socket.onopen = () => { + console.log('WebSocket connection established.') + isConnected.value = true + } + + socket.onmessage = (event) => { + try { + const data = JSON.parse(event.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.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') + }, 100) + return + } + + newDataBuffer.push(data) + } catch (error) { + console.error('Error parsing incoming data:', error) + } + } + + socket.onclose = (event) => { + console.log(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`) + isConnected.value = false + socket = null + } + + socket.onerror = (error) => { + console.error('WebSocket error:', error) + isConnected.value = false + if (socket) { + socket = null + } + } + + // Process the buffer at intervals + setInterval(() => { + if (newDataBuffer.length > 0) { + const data = newDataBuffer.shift() + if (data) { + processIncomingData(data) + } + } + }, 500) + } + + function disconnect() { + if (socket) { + socket.close() + } + } + + function isLegacyData(data: any): data is LegacyEnergyData { + return 'value' in data && !('energy' in data) + } + + function processIncomingData(data: LegacyEnergyData | SensorReading) { + // Skip non-data messages + if ( + 'type' in data && + (data.type === 'connection_established' || data.type === 'proxy_info') + ) { + console.log('Received system message:', data.type) + return + } + + const sensorStore = useSensorStore() + const roomStore = useRoomStore() + + // Handle both legacy and new data formats + if (isLegacyData(data)) { + latestMessage.value = data + sensorStore.updateSensorData(data) + + // 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) + + // Update time series for chart (use energy values) + const newLabel = new Date(data.timestamp * 1000).toLocaleTimeString() + timeSeriesData.labels.push(newLabel) + timeSeriesData.datasets[0].data.push(data.energy.value) + } + + // Keep only the latest data points + if (timeSeriesData.labels.length > MAX_DATA_POINTS) { + timeSeriesData.labels.shift() + timeSeriesData.datasets[0].data.shift() + } + } + + return { + // State + isConnected, + latestMessage, + timeSeriesData, + + // Actions + connect, + disconnect, + } +}) \ No newline at end of file