Refactor API and store logic for sensor and room management
- Add sensor name to SensorInfo interface - Update API client to conditionally include auth headers - Add saveToken endpoint to authApi - Refactor roomsApi to use getRoomNames endpoint - Change sensorsApi to use /api/v1/sensors/get - Improve token handling and JWT decoding in auth store - Refactor room loading to use API client in energy store - Add helper to transform API sensor data - Update SensorManagementView to load sensors from API and fix filtering
This commit is contained in:
@@ -76,6 +76,7 @@ export interface SensorReading {
|
||||
}
|
||||
|
||||
export interface SensorInfo {
|
||||
name: string
|
||||
sensor_id: string
|
||||
sensor_type: SensorType
|
||||
room?: string
|
||||
@@ -211,13 +212,49 @@ class ApiClient {
|
||||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
private shouldIncludeAuth(endpoint: string): boolean {
|
||||
// Endpoints that do NOT require authentication
|
||||
const publicEndpoints = [
|
||||
'/health',
|
||||
'/api/v1/tokens/generate',
|
||||
'/api/v1/tokens/validate',
|
||||
'/api/v1/ingestion/',
|
||||
]
|
||||
|
||||
// Special case: token save, revoke operations might need auth depending on backend implementation
|
||||
// For now, let's include auth for them to be safe
|
||||
return !publicEndpoints.some((publicPath) => endpoint.startsWith(publicPath))
|
||||
}
|
||||
|
||||
private getAuthHeaders(endpoint: string): Record<string, string> {
|
||||
// Only include auth headers for endpoints that require authentication
|
||||
if (!this.shouldIncludeAuth(endpoint)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Dynamically get auth headers to avoid circular imports
|
||||
try {
|
||||
// Try to get from window first (for when store is exposed)
|
||||
const authStore = (window as any).__AUTH_STORE__
|
||||
if (authStore && typeof authStore.getAuthHeader === 'function') {
|
||||
return authStore.getAuthHeader()
|
||||
}
|
||||
|
||||
// Fallback: try to access the auth store directly
|
||||
// This requires the auth store to be initialized
|
||||
const token = localStorage.getItem('dashboard_auth_token')
|
||||
if (token) {
|
||||
// Check if token is still valid
|
||||
const expiry = localStorage.getItem('dashboard_token_expiry')
|
||||
if (expiry) {
|
||||
const expiryTime = new Date(expiry).getTime()
|
||||
const currentTime = new Date().getTime()
|
||||
|
||||
if (currentTime < expiryTime) {
|
||||
return { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not get auth headers:', error)
|
||||
}
|
||||
@@ -227,7 +264,7 @@ class ApiClient {
|
||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`
|
||||
|
||||
const authHeaders = this.getAuthHeaders()
|
||||
const authHeaders = this.getAuthHeaders(endpoint)
|
||||
const config: RequestInit = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -270,7 +307,7 @@ class ApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
const authHeaders = this.getAuthHeaders()
|
||||
const authHeaders = this.getAuthHeaders(endpoint)
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* Authentication API Service
|
||||
* Handles JWT token generation and validation
|
||||
*/
|
||||
import { apiClient } from './api'
|
||||
|
||||
export interface TokenRequest {
|
||||
name: string
|
||||
list_of_resources: string[]
|
||||
data_aggregation?: boolean
|
||||
time_aggregation?: boolean
|
||||
embargo?: number
|
||||
exp_hours?: number
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
token: string
|
||||
expires_at: string
|
||||
resources: string[]
|
||||
}
|
||||
|
||||
export interface TokenValidation {
|
||||
@@ -26,6 +24,10 @@ export const authApi = {
|
||||
return apiClient.post<TokenResponse>('/api/v1/tokens/generate', request)
|
||||
},
|
||||
|
||||
async saveToken(token: string): Promise<{ token: string; datetime: string; active: boolean }> {
|
||||
return apiClient.post<{ token: string; datetime: string; active: boolean }>('/api/v1/tokens/save', { token })
|
||||
},
|
||||
|
||||
async validateToken(token: string): Promise<TokenValidation> {
|
||||
return apiClient.post<TokenValidation>('/api/v1/tokens/validate', { token })
|
||||
},
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
/**
|
||||
* 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 getRoomNames(): Promise<{ rooms: string[] }> {
|
||||
return apiClient.get<{ rooms: string[] }>('/api/v1/rooms/names')
|
||||
},
|
||||
|
||||
async getRooms(): Promise<RoomInfo[]> {
|
||||
return apiClient.get<RoomInfo[]>('/api/v1/rooms')
|
||||
},
|
||||
|
||||
/**
|
||||
* Get historical data for a specific room
|
||||
*/
|
||||
async getRoomData(
|
||||
roomName: string,
|
||||
params?: {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const sensorsApi = {
|
||||
sensor_type?: SensorType
|
||||
status?: SensorStatus
|
||||
}): Promise<SensorInfo[]> {
|
||||
return apiClient.get<SensorInfo[]>('/api/v1/sensors', params)
|
||||
return apiClient.get<SensorInfo[]>('/api/v1/sensors/get', params)
|
||||
},
|
||||
|
||||
async getSensor(sensorId: string): Promise<SensorInfo> {
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { authApi, type TokenRequest, type TokenResponse } from '@/services/authApi'
|
||||
import { authApi, type TokenRequest } from '@/services/authApi'
|
||||
|
||||
// Helper function to decode JWT token payload (without verification)
|
||||
function decodeJwtPayload(token: string) {
|
||||
try {
|
||||
const base64Url = token.split('.')[1]
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join(''),
|
||||
)
|
||||
return JSON.parse(jsonPayload)
|
||||
} catch (error) {
|
||||
console.error('Failed to decode JWT payload:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const TOKEN_STORAGE_KEY = 'dashboard_auth_token'
|
||||
const TOKEN_EXPIRY_KEY = 'dashboard_token_expiry'
|
||||
@@ -53,30 +71,35 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
try {
|
||||
const request: TokenRequest = {
|
||||
name,
|
||||
list_of_resources: [
|
||||
'sensors',
|
||||
'rooms',
|
||||
'analytics',
|
||||
'health',
|
||||
'data',
|
||||
'export',
|
||||
'events'
|
||||
]
|
||||
list_of_resources: ['sensors', 'rooms', 'analytics', 'health', 'data', 'export', 'events'],
|
||||
data_aggregation: true,
|
||||
time_aggregation: true,
|
||||
embargo: 0,
|
||||
exp_hours: 24,
|
||||
}
|
||||
|
||||
const response = await authApi.generateToken(request)
|
||||
|
||||
// Save token to backend database immediately after generation
|
||||
await authApi.saveToken(response.token)
|
||||
|
||||
// Decode the JWT to get expiration and resources
|
||||
const payload = decodeJwtPayload(response.token)
|
||||
if (!payload) {
|
||||
throw new Error('Failed to decode token payload')
|
||||
}
|
||||
|
||||
// Store token data
|
||||
token.value = response.token
|
||||
tokenExpiry.value = response.expires_at
|
||||
tokenResources.value = response.resources || request.list_of_resources
|
||||
tokenExpiry.value = new Date(payload.exp * 1000).toISOString()
|
||||
tokenResources.value = payload.list_of_resources || request.list_of_resources
|
||||
|
||||
// Persist to localStorage
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, response.token)
|
||||
localStorage.setItem(TOKEN_EXPIRY_KEY, response.expires_at)
|
||||
localStorage.setItem(TOKEN_EXPIRY_KEY, tokenExpiry.value)
|
||||
localStorage.setItem(TOKEN_RESOURCES_KEY, JSON.stringify(tokenResources.value))
|
||||
|
||||
console.log('Authentication successful, token expires at:', response.expires_at)
|
||||
console.log('Authentication successful, token expires at:', tokenExpiry.value)
|
||||
return true
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to generate token'
|
||||
@@ -123,7 +146,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
localStorage.removeItem(TOKEN_RESOURCES_KEY)
|
||||
}
|
||||
|
||||
function loadTokenFromStorage() {
|
||||
async function loadTokenFromStorage() {
|
||||
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||
const storedExpiry = localStorage.getItem(TOKEN_EXPIRY_KEY)
|
||||
const storedResources = localStorage.getItem(TOKEN_RESOURCES_KEY)
|
||||
@@ -159,7 +182,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
async function ensureAuthenticated(): Promise<boolean> {
|
||||
// Try to load from storage first
|
||||
if (loadTokenFromStorage() && isAuthenticated.value) {
|
||||
if ((await loadTokenFromStorage()) && isAuthenticated.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -175,8 +198,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return {}
|
||||
}
|
||||
|
||||
// Initialize on store creation
|
||||
loadTokenFromStorage()
|
||||
// Initialize on store creation (async)
|
||||
loadTokenFromStorage().catch(error => {
|
||||
console.warn('Failed to load token from storage:', error)
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
@@ -196,6 +221,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
clearToken,
|
||||
loadTokenFromStorage,
|
||||
ensureAuthenticated,
|
||||
getAuthHeader
|
||||
getAuthHeader,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -207,7 +207,10 @@ export const useEnergyStore = defineStore('energy', () => {
|
||||
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')) {
|
||||
if (
|
||||
'type' in data &&
|
||||
(data.type === 'connection_established' || data.type === 'proxy_info')
|
||||
) {
|
||||
console.log('Received system message:', data.type)
|
||||
return
|
||||
}
|
||||
@@ -339,6 +342,331 @@ export const useEnergyStore = defineStore('energy', () => {
|
||||
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<void> => {
|
||||
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<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'
|
||||
|
||||
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<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
|
||||
|
||||
// Initialize mock sensor devices
|
||||
function initializeMockSensors() {
|
||||
const mockSensors: SensorDevice[] = [
|
||||
@@ -470,322 +798,7 @@ export const useEnergyStore = defineStore('energy', () => {
|
||||
sensorDevices.set(sensor.id, sensor)
|
||||
})
|
||||
}
|
||||
|
||||
// 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<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
|
||||
|
||||
// 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<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'
|
||||
|
||||
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()
|
||||
//initializeMockSensors()
|
||||
|
||||
// Load rooms from API on store initialization
|
||||
loadRoomsFromAPI()
|
||||
|
||||
@@ -24,15 +24,6 @@
|
||||
<!-- 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"
|
||||
>
|
||||
<option value="">All Rooms</option>
|
||||
<option v-for="room in energyStore.availableRooms" :key="room" :value="room">
|
||||
{{ room }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
@click="showRoomManagementModal = true"
|
||||
class="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-1"
|
||||
@@ -48,6 +39,15 @@
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Rooms</span>
|
||||
</button>
|
||||
<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 }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<select v-model="selectedType" class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
|
||||
@@ -241,13 +241,14 @@ const isExecutingAction = ref(false)
|
||||
const showRoomManagementModal = ref(false)
|
||||
|
||||
const sensorList = computed(() => {
|
||||
console.log(energyStore.sensorDevices)
|
||||
return Array.from(energyStore.sensorDevices.values()).sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
|
||||
const filteredSensors = computed(() => {
|
||||
return sensorList.value.filter((sensor) => {
|
||||
const matchesRoom = !selectedRoom.value || sensor.room === selectedRoom.value
|
||||
const matchesType = !selectedType.value || sensor.type === selectedType.value
|
||||
const matchesType = !selectedType.value || sensor.sensor_type === selectedType.value
|
||||
const matchesStatus = !selectedStatus.value || sensor.status === selectedStatus.value
|
||||
|
||||
return matchesRoom && matchesType && matchesStatus
|
||||
@@ -362,8 +363,12 @@ const getOccupancyColor = (occupancy: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket connection for real-time updates
|
||||
onMounted(() => {
|
||||
// Load sensors from API and connect WebSocket for real-time updates
|
||||
onMounted(async () => {
|
||||
// First load sensors from database
|
||||
await energyStore.fetchApiSensors()
|
||||
|
||||
// Then connect WebSocket for real-time updates
|
||||
if (!energyStore.isConnected) {
|
||||
energyStore.connect('ws://localhost:8000/ws')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user