Refactor stores for modularity and API type updates
- Split energy store into sensor, room, analytics, and websocket stores - Add new analytics, room, sensor, and websocket stores - Update API types for sensors (SensorDevice, SensorAction) - Update sensorsApi to use new SensorDevice type - Add central index for store exports and types - Refactor energy store to delegate to modular stores - Remove legacy code and consolidate API logic
This commit is contained in:
@@ -75,19 +75,6 @@ export interface SensorReading {
|
|||||||
metadata?: Record<string, any>
|
metadata?: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
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<string, any>
|
|
||||||
metadata?: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoomInfo {
|
export interface RoomInfo {
|
||||||
room: string
|
room: string
|
||||||
sensor_count: number
|
sensor_count: number
|
||||||
@@ -188,6 +175,60 @@ export interface SystemEvent {
|
|||||||
room?: string
|
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 {
|
export enum SensorType {
|
||||||
ENERGY = 'energy',
|
ENERGY = 'energy',
|
||||||
CO2 = 'co2',
|
CO2 = 'co2',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
apiClient,
|
apiClient,
|
||||||
type SensorInfo,
|
type SensorDevice,
|
||||||
type SensorReading,
|
type SensorReading,
|
||||||
type DataQuery,
|
type DataQuery,
|
||||||
type DataResponse,
|
type DataResponse,
|
||||||
@@ -13,12 +13,12 @@ export const sensorsApi = {
|
|||||||
room?: string
|
room?: string
|
||||||
sensor_type?: SensorType
|
sensor_type?: SensorType
|
||||||
status?: SensorStatus
|
status?: SensorStatus
|
||||||
}): Promise<SensorInfo[]> {
|
}): Promise<SensorDevice[]> {
|
||||||
return apiClient.get<SensorInfo[]>('/api/v1/sensors/get', params)
|
return apiClient.get<SensorDevice[]>('/api/v1/sensors/get', params)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getSensor(sensorId: string): Promise<SensorInfo> {
|
async getSensor(sensorId: string): Promise<SensorDevice> {
|
||||||
return apiClient.get<SensorInfo>(`/api/v1/sensors/${sensorId}`)
|
return apiClient.get<SensorDevice>(`/api/v1/sensors/${sensorId}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getSensorData(
|
async getSensorData(
|
||||||
|
|||||||
151
src/stores/analytics.ts
Normal file
151
src/stores/analytics.ts
Normal file
@@ -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<SystemStatus | null>(null)
|
||||||
|
const healthStatus = ref<HealthCheck | null>(null)
|
||||||
|
const apiLoading = ref(false)
|
||||||
|
const apiError = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,858 +1,93 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, reactive } from 'vue'
|
import { computed } from 'vue'
|
||||||
import {
|
import { useSensorStore } from './sensor'
|
||||||
sensorsApi,
|
import { useRoomStore } from './room'
|
||||||
roomsApi,
|
import { useAnalyticsStore } from './analytics'
|
||||||
analyticsApi,
|
import { useWebSocketStore } from './websocket'
|
||||||
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', () => {
|
export const useEnergyStore = defineStore('energy', () => {
|
||||||
// State
|
// Get instances of other stores
|
||||||
const isConnected = ref(false)
|
const sensorStore = useSensorStore()
|
||||||
const latestMessage = ref<LegacyEnergyData | null>(null)
|
const roomStore = useRoomStore()
|
||||||
const timeSeriesData = reactive<{
|
const analyticsStore = useAnalyticsStore()
|
||||||
labels: string[]
|
const webSocketStore = useWebSocketStore()
|
||||||
datasets: { data: number[] }[]
|
|
||||||
}>({
|
// Delegate to WebSocket store
|
||||||
labels: [],
|
const connect = (url: string) => webSocketStore.connect(url)
|
||||||
datasets: [{ data: [] }],
|
const disconnect = () => webSocketStore.disconnect()
|
||||||
})
|
|
||||||
|
|
||||||
const sensorsData = reactive<Map<string, any>>(new Map()) // Legacy support
|
|
||||||
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[]>([])
|
|
||||||
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 && 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<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
|
// Initialize data from APIs
|
||||||
async function initializeFromApi() {
|
async function initializeFromApi() {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
loadRoomsFromAPI(), // Load room names first
|
roomStore.loadRoomsFromAPI(),
|
||||||
fetchApiSensors(),
|
sensorStore.fetchApiSensors(),
|
||||||
fetchApiRooms(),
|
roomStore.fetchApiRooms(),
|
||||||
fetchAnalyticsSummary(),
|
analyticsStore.initializeAnalyticsFromApi(),
|
||||||
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 {
|
return {
|
||||||
// WebSocket state
|
// WebSocket state (delegated)
|
||||||
isConnected,
|
isConnected: computed(() => webSocketStore.isConnected),
|
||||||
latestMessage,
|
latestMessage: computed(() => webSocketStore.latestMessage),
|
||||||
timeSeriesData,
|
timeSeriesData: computed(() => webSocketStore.timeSeriesData),
|
||||||
sensorsData,
|
|
||||||
roomsData,
|
|
||||||
latestReadings,
|
|
||||||
sensorDevices,
|
|
||||||
availableRooms,
|
|
||||||
roomsLoading,
|
|
||||||
roomsLoaded,
|
|
||||||
|
|
||||||
// API state
|
// Sensor state (delegated)
|
||||||
apiSensors,
|
sensorsData: computed(() => sensorStore.sensorsData),
|
||||||
apiRooms,
|
sensorDevices: computed(() => sensorStore.sensorDevices),
|
||||||
analyticsData,
|
latestReadings: computed(() => sensorStore.latestReadings),
|
||||||
systemStatus,
|
apiSensors: computed(() => Array.from(sensorStore.sensorDevices.values())), // Convert Map to Array
|
||||||
healthStatus,
|
|
||||||
apiLoading,
|
|
||||||
apiError,
|
|
||||||
|
|
||||||
// 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,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
getCO2Status,
|
|
||||||
updateSensorRoom,
|
|
||||||
executeSensorAction,
|
|
||||||
getSensorsByRoom,
|
|
||||||
getSensorsByType,
|
|
||||||
loadRoomsFromAPI,
|
|
||||||
addRoom,
|
|
||||||
removeRoom,
|
|
||||||
getRoomStats,
|
|
||||||
getAllRoomsWithStats,
|
|
||||||
|
|
||||||
// API functions
|
// Sensor functions (delegated)
|
||||||
fetchApiSensors,
|
updateSensorRoom: sensorStore.updateSensorRoom,
|
||||||
fetchApiSensorData,
|
executeSensorAction: sensorStore.executeSensorAction,
|
||||||
updateApiSensorMetadata,
|
getSensorsByRoom: sensorStore.getSensorsByRoom,
|
||||||
deleteApiSensor,
|
getSensorsByType: sensorStore.getSensorsByType,
|
||||||
exportApiData,
|
fetchApiSensors: sensorStore.fetchApiSensors,
|
||||||
fetchApiRooms,
|
fetchApiSensorData: sensorStore.fetchApiSensorData,
|
||||||
fetchApiRoomData,
|
updateApiSensorMetadata: sensorStore.updateApiSensorMetadata,
|
||||||
fetchAnalyticsSummary,
|
deleteApiSensor: sensorStore.deleteApiSensor,
|
||||||
fetchEnergyTrends,
|
exportApiData: sensorStore.exportApiData,
|
||||||
fetchRoomComparison,
|
|
||||||
fetchSystemEvents,
|
// Room functions (delegated)
|
||||||
fetchSystemStatus,
|
getCO2Status: roomStore.getCO2Status,
|
||||||
fetchHealthStatus,
|
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,
|
initializeFromApi,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
16
src/stores/index.ts
Normal file
16
src/stores/index.ts
Normal file
@@ -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'
|
||||||
298
src/stores/room.ts
Normal file
298
src/stores/room.ts
Normal file
@@ -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<Map<string, RoomMetrics>>(new Map())
|
||||||
|
const availableRooms = ref<string[]>([])
|
||||||
|
const apiRooms = ref<ApiRoomInfo[]>([])
|
||||||
|
const roomsLoading = ref<boolean>(false)
|
||||||
|
const roomsLoaded = ref<boolean>(false)
|
||||||
|
const apiLoading = ref(false)
|
||||||
|
const apiError = ref<string | null>(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<void> => {
|
||||||
|
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<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 {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
355
src/stores/sensor.ts
Normal file
355
src/stores/sensor.ts
Normal file
@@ -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<Map<string, SensorDevice>>(new Map())
|
||||||
|
const latestReadings = reactive<Map<string, SensorReading>>(new Map())
|
||||||
|
const sensorsData = reactive<Map<string, any>>(new Map()) // Legacy support
|
||||||
|
const apiLoading = ref(false)
|
||||||
|
const apiError = ref<string | null>(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<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 {
|
||||||
|
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<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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
})
|
||||||
186
src/stores/websocket.ts
Normal file
186
src/stores/websocket.ts
Normal file
@@ -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<LegacyEnergyData | null>(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,
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user