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.
This commit is contained in:
rafaeldpsilva
2025-09-18 14:29:44 +01:00
parent faed09d3b6
commit a3d266d735
4 changed files with 468 additions and 29 deletions

30
src/services/roomsApi.ts Normal file
View File

@@ -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<RoomInfo[]> {
return apiClient.get<RoomInfo[]>('/api/v1/rooms')
},
/**
* Get historical data for a specific room
*/
async getRoomData(
roomName: string,
params?: {
start_time?: number
end_time?: number
limit?: number
},
): Promise<RoomData> {
return apiClient.get<RoomData>(`/api/v1/rooms/${encodeURIComponent(roomName)}/data`, params)
},
}
export default roomsApi

View File

@@ -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<SensorInfo[]> {
return apiClient.get<SensorInfo[]>('/api/v1/sensors', params)
},
/**
* Get detailed information about a specific sensor
*/
async getSensor(sensorId: string): Promise<SensorInfo> {
return apiClient.get<SensorInfo>(`/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<DataResponse> {
return apiClient.get<DataResponse>(`/api/v1/sensors/${sensorId}/data`, params)
},
/**
* Advanced data query with multiple filters
*/
async queryData(query: DataQuery): Promise<DataResponse> {
return apiClient.post<DataResponse>('/api/v1/data/query', query)
},
/**
* Update sensor metadata
*/
async updateSensorMetadata(
sensorId: string,
metadata: Record<string, any>,
): 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

View File

@@ -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<Map<string, RoomMetrics>>(new Map())
const latestReadings = reactive<Map<string, SensorReading>>(new Map())
const sensorDevices = reactive<Map<string, SensorDevice>>(new Map())
const availableRooms = ref<string[]>([
'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<string[]>([])
const roomsLoading = ref<boolean>(false)
const roomsLoaded = ref<boolean>(false)
// API integration state
const apiSensors = ref<ApiSensorInfo[]>([])
const apiRooms = ref<ApiRoomInfo[]>([])
const analyticsData = ref<{
summary: AnalyticsSummary | null
trends: EnergyTrends | null
roomComparison: RoomComparison | null
}>({
summary: null,
trends: null,
roomComparison: null
})
const systemStatus = ref<SystemStatus | null>(null)
const healthStatus = ref<HealthCheck | null>(null)
const apiLoading = ref(false)
const apiError = ref<string | null>(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,6 +206,12 @@ 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
@@ -426,6 +485,55 @@ export const useEnergyStore = defineStore('energy', () => {
}
// Room management functions
const loadRoomsFromAPI = async (): Promise<void> => {
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
@@ -487,10 +595,178 @@ export const useEnergyStore = defineStore('energy', () => {
}))
}
// API Integration Functions
async function handleApiCall<T>(apiCall: () => Promise<T>): Promise<T | null> {
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<string, any>) {
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
}
})

View File

@@ -24,7 +24,10 @@
<!-- Filters -->
<div class="flex flex-col sm:flex-row gap-4 flex-1">
<div class="flex gap-2">
<select v-model="selectedRoom" class="px-4 py-2 border border-gray-200 rounded-lg bg-white flex-1">
<select
v-model="selectedRoom"
class="px-4 py-2 border border-gray-200 rounded-lg bg-white flex-1"
>
<option value="">All Rooms</option>
<option v-for="room in energyStore.availableRooms" :key="room" :value="room">
{{ room }}
@@ -36,7 +39,12 @@
title="Manage Rooms"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
<span class="hidden sm:inline">Rooms</span>
</button>
@@ -201,10 +209,7 @@
/>
<!-- Room Management Modal -->
<RoomManagementModal
v-if="showRoomManagementModal"
@close="showRoomManagementModal = false"
/>
<RoomManagementModal v-if="showRoomManagementModal" @close="showRoomManagementModal = false" />
</div>
</template>
@@ -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')
}
})