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[] } } 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 } // Initialize data from APIs async function initializeFromApi() { await Promise.allSettled([ loadRoomsFromAPI(), // Load room names first fetchApiSensors(), fetchApiRooms(), fetchAnalyticsSummary(), fetchSystemStatus(), fetchHealthStatus(), ]) } // 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, // API state apiSensors, apiRooms, analyticsData, systemStatus, healthStatus, apiLoading, apiError, // WebSocket functions 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, initializeFromApi, } })