From a3d266d735d09cbf6c02a1b386c2c3fb45ac6b36 Mon Sep 17 00:00:00 2001 From: rafaeldpsilva Date: Thu, 18 Sep 2025 14:29:44 +0100 Subject: [PATCH] Add API integration for sensors and rooms management Integrate sensorsApi and roomsApi services into energy store. Add API state, loading/error handling, and async functions for fetching sensor and room data. Update room loading logic to fetch from API. Expose new API functions for analytics and health endpoints. Update SensorManagementView to use localhost WebSocket for real-time updates. --- src/services/roomsApi.ts | 30 +++ src/services/sensorsApi.ts | 98 ++++++++ src/stores/energy.ts | 350 +++++++++++++++++++++++++++-- src/views/SensorManagementView.vue | 19 +- 4 files changed, 468 insertions(+), 29 deletions(-) create mode 100644 src/services/roomsApi.ts create mode 100644 src/services/sensorsApi.ts diff --git a/src/services/roomsApi.ts b/src/services/roomsApi.ts new file mode 100644 index 0000000..56b7bcd --- /dev/null +++ b/src/services/roomsApi.ts @@ -0,0 +1,30 @@ +/** + * Rooms API Service + * Handles room-related API calls + */ +import { apiClient, type RoomInfo, type RoomData } from './api' + +export const roomsApi = { + /** + * Get list of all rooms with sensor counts and latest metrics + */ + async getRooms(): Promise { + return apiClient.get('/api/v1/rooms') + }, + + /** + * Get historical data for a specific room + */ + async getRoomData( + roomName: string, + params?: { + start_time?: number + end_time?: number + limit?: number + }, + ): Promise { + return apiClient.get(`/api/v1/rooms/${encodeURIComponent(roomName)}/data`, params) + }, +} + +export default roomsApi diff --git a/src/services/sensorsApi.ts b/src/services/sensorsApi.ts new file mode 100644 index 0000000..bfc68d3 --- /dev/null +++ b/src/services/sensorsApi.ts @@ -0,0 +1,98 @@ +/** + * Sensors API Service + * Handles sensor-related API calls + */ +import { + apiClient, + type SensorInfo, + type SensorReading, + type DataQuery, + type DataResponse, + type SensorType, + type SensorStatus, +} from './api' + +export const sensorsApi = { + /** + * Get all sensors with optional filtering + */ + async getSensors(params?: { + room?: string + sensor_type?: SensorType + status?: SensorStatus + }): Promise { + return apiClient.get('/api/v1/sensors', params) + }, + + /** + * Get detailed information about a specific sensor + */ + async getSensor(sensorId: string): Promise { + return apiClient.get(`/api/v1/sensors/${sensorId}`) + }, + + /** + * Get historical data for a specific sensor + */ + async getSensorData( + sensorId: string, + params?: { + start_time?: number + end_time?: number + limit?: number + offset?: number + }, + ): Promise { + return apiClient.get(`/api/v1/sensors/${sensorId}/data`, params) + }, + + /** + * Advanced data query with multiple filters + */ + async queryData(query: DataQuery): Promise { + return apiClient.post('/api/v1/data/query', query) + }, + + /** + * Update sensor metadata + */ + async updateSensorMetadata( + sensorId: string, + metadata: Record, + ): Promise<{ message: string }> { + return apiClient.put<{ message: string }>(`/api/v1/sensors/${sensorId}/metadata`, metadata) + }, + + /** + * Delete sensor and all its data + */ + async deleteSensor(sensorId: string): Promise<{ + message: string + readings_deleted: number + metadata_deleted?: boolean + }> { + return apiClient.delete(`/api/v1/sensors/${sensorId}`) + }, + + /** + * Export sensor data for specified time range + */ + async exportData(params: { + start_time: number + end_time: number + sensor_ids?: string + format?: 'json' | 'csv' + }): Promise<{ + data: SensorReading[] + count: number + export_params: any + }> { + return apiClient.get<{ + data: SensorReading[] + count: number + export_params: any + }>('/api/v1/export', params) + }, +} + +export default sensorsApi diff --git a/src/stores/energy.ts b/src/stores/energy.ts index d71f51a..2abfeff 100644 --- a/src/stores/energy.ts +++ b/src/stores/energy.ts @@ -1,5 +1,18 @@ 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 @@ -96,29 +109,46 @@ export const useEnergyStore = defineStore('energy', () => { const roomsData = reactive>(new Map()) const latestReadings = reactive>(new Map()) const sensorDevices = reactive>(new Map()) - const availableRooms = ref([ - 'Conference Room A', - 'Conference Room B', - 'Office Floor 1', - 'Office Floor 2', - 'Kitchen', - 'Lobby', - 'Server Room', - 'Storage Room', - 'Meeting Room 1', - 'Meeting Room 2' - ]) + 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) { + 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) @@ -130,14 +160,35 @@ export const useEnergyStore = defineStore('energy', () => { 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 = () => { - console.log('WebSocket connection closed.') + socket.onclose = (event) => { + console.log(`WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`) isConnected.value = false socket = null } @@ -145,7 +196,9 @@ export const useEnergyStore = defineStore('energy', () => { socket.onerror = (error) => { console.error('WebSocket error:', error) isConnected.value = false - socket = null + if (socket) { + socket = null + } } // Process the buffer at intervals @@ -153,11 +206,17 @@ export const useEnergyStore = defineStore('energy', () => { 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 (data.type && (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) @@ -165,7 +224,7 @@ export const useEnergyStore = defineStore('energy', () => { } 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) @@ -426,18 +485,67 @@ export const useEnergyStore = defineStore('energy', () => { } // Room management functions + const loadRoomsFromAPI = async (): Promise => { + if (roomsLoading.value || roomsLoaded.value) { + return // Already loading or loaded + } + + roomsLoading.value = true + + try { + // Try to load from microservices API first + const response = await fetch('/api/v1/rooms/names') + + if (response.ok) { + const data = await response.json() + if (data.rooms && Array.isArray(data.rooms)) { + availableRooms.value = data.rooms.sort() + roomsLoaded.value = true + console.log('Loaded rooms from microservices API:', data.rooms.length) + return + } + } + + // Fallback: try direct sensor service connection + const directResponse = await fetch('http://localhost:8007/rooms/names') + + if (directResponse.ok) { + const data = await directResponse.json() + if (data.rooms && Array.isArray(data.rooms)) { + availableRooms.value = data.rooms.sort() + roomsLoaded.value = true + console.log('Loaded rooms from sensor service:', data.rooms.length) + return + } + } + + // If both fail, use empty list and log warning + console.warn('Failed to load rooms from API, 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 } @@ -487,10 +595,178 @@ export const useEnergyStore = defineStore('energy', () => { })) } + // 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' + + // Handle authentication errors + 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 + } + } + + // Sensors API functions + async function fetchApiSensors(params?: { room?: string; sensor_type?: any; status?: any }) { + const result = await handleApiCall(() => sensorsApi.getSensors(params)) + if (result) { + apiSensors.value = result + } + 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 initializeMockSensors() + // Load rooms from API on store initialization + loadRoomsFromAPI() + return { + // WebSocket state isConnected, latestMessage, timeSeriesData, @@ -499,6 +775,19 @@ export const useEnergyStore = defineStore('energy', () => { latestReadings, sensorDevices, availableRooms, + roomsLoading, + roomsLoaded, + + // API state + apiSensors, + apiRooms, + analyticsData, + systemStatus, + healthStatus, + apiLoading, + apiError, + + // WebSocket functions connect, disconnect, getCO2Status, @@ -506,9 +795,26 @@ export const useEnergyStore = defineStore('energy', () => { executeSensorAction, getSensorsByRoom, getSensorsByType, + loadRoomsFromAPI, addRoom, removeRoom, getRoomStats, - getAllRoomsWithStats + getAllRoomsWithStats, + + // API functions + fetchApiSensors, + fetchApiSensorData, + updateApiSensorMetadata, + deleteApiSensor, + exportApiData, + fetchApiRooms, + fetchApiRoomData, + fetchAnalyticsSummary, + fetchEnergyTrends, + fetchRoomComparison, + fetchSystemEvents, + fetchSystemStatus, + fetchHealthStatus, + initializeFromApi } }) diff --git a/src/views/SensorManagementView.vue b/src/views/SensorManagementView.vue index fec162c..35618cf 100644 --- a/src/views/SensorManagementView.vue +++ b/src/views/SensorManagementView.vue @@ -24,7 +24,10 @@
-
@@ -360,7 +365,7 @@ const getOccupancyColor = (occupancy: string) => { // WebSocket connection for real-time updates onMounted(() => { if (!energyStore.isConnected) { - energyStore.connect('ws://192.168.1.73:8000/ws') + energyStore.connect('ws://localhost:8000/ws') } })