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:
rafaeldpsilva
2025-09-25 17:09:42 +01:00
parent 326746b5ef
commit 3299472c85
8 changed files with 1139 additions and 857 deletions

View File

@@ -75,19 +75,6 @@ export interface SensorReading {
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 {
room: string
sensor_count: number
@@ -188,6 +175,60 @@ export interface SystemEvent {
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 {
ENERGY = 'energy',
CO2 = 'co2',

View File

@@ -1,6 +1,6 @@
import {
apiClient,
type SensorInfo,
type SensorDevice,
type SensorReading,
type DataQuery,
type DataResponse,
@@ -13,12 +13,12 @@ export const sensorsApi = {
room?: string
sensor_type?: SensorType
status?: SensorStatus
}): Promise<SensorInfo[]> {
return apiClient.get<SensorInfo[]>('/api/v1/sensors/get', params)
}): Promise<SensorDevice[]> {
return apiClient.get<SensorDevice[]>('/api/v1/sensors/get', params)
},
async getSensor(sensorId: string): Promise<SensorInfo> {
return apiClient.get<SensorInfo>(`/api/v1/sensors/${sensorId}`)
async getSensor(sensorId: string): Promise<SensorDevice> {
return apiClient.get<SensorDevice>(`/api/v1/sensors/${sensorId}`)
},
async getSensorData(

151
src/stores/analytics.ts Normal file
View 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,
}
})

View File

@@ -1,858 +1,93 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
import {
sensorsApi,
roomsApi,
analyticsApi,
healthApi,
type SensorInfo as ApiSensorInfo,
type RoomInfo as ApiRoomInfo,
type AnalyticsSummary,
type EnergyTrends,
type RoomComparison,
type SystemStatus,
type HealthCheck,
} from '@/services'
const MAX_DATA_POINTS = 100 // Keep the last 100 data points for the chart
interface SensorReading {
sensorId: string
room: string
timestamp: number
energy: {
value: number
unit: string
}
co2: {
value: number
unit: string
}
temperature?: {
value: number
unit: string
}
}
interface RoomMetrics {
room: string
sensors: string[]
energy: {
current: number
total: number
average: number
unit: string
}
co2: {
current: number
average: number
max: number
status: 'good' | 'moderate' | 'poor' | 'critical'
unit: string
}
occupancyEstimate: 'low' | 'medium' | 'high'
lastUpdated: number
}
interface LegacyEnergyData {
sensorId: string
timestamp: number
value: number
unit: string
}
interface SensorDevice {
id: string
name: string
type: 'energy' | 'co2' | 'temperature' | 'humidity' | 'hvac' | 'lighting' | 'security'
room: string
status: 'online' | 'offline' | 'error'
lastSeen: number
capabilities: {
monitoring: string[] // e.g., ['energy', 'temperature']
actions: SensorAction[] // Available actions
}
metadata: {
location: string
model?: string
firmware?: string
battery?: number
}
}
interface SensorAction {
id: string
name: string
type: 'toggle' | 'adjust' | 'trigger'
icon: string
parameters?: {
min?: number
max?: number
step?: number
options?: string[]
}
}
import { computed } from 'vue'
import { useSensorStore } from './sensor'
import { useRoomStore } from './room'
import { useAnalyticsStore } from './analytics'
import { useWebSocketStore } from './websocket'
export const useEnergyStore = defineStore('energy', () => {
// State
const isConnected = ref(false)
const latestMessage = ref<LegacyEnergyData | null>(null)
const timeSeriesData = reactive<{
labels: string[]
datasets: { data: number[] }[]
}>({
labels: [],
datasets: [{ data: [] }],
})
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
}
// Get instances of other stores
const sensorStore = useSensorStore()
const roomStore = useRoomStore()
const analyticsStore = useAnalyticsStore()
const webSocketStore = useWebSocketStore()
// Delegate to WebSocket store
const connect = (url: string) => webSocketStore.connect(url)
const disconnect = () => webSocketStore.disconnect()
// Initialize data from APIs
async function initializeFromApi() {
await Promise.allSettled([
loadRoomsFromAPI(), // Load room names first
fetchApiSensors(),
fetchApiRooms(),
fetchAnalyticsSummary(),
fetchSystemStatus(),
fetchHealthStatus(),
roomStore.loadRoomsFromAPI(),
sensorStore.fetchApiSensors(),
roomStore.fetchApiRooms(),
analyticsStore.initializeAnalyticsFromApi(),
])
}
// Initialize mock sensors on store creation
// Initialize mock sensor devices
function initializeMockSensors() {
const mockSensors: SensorDevice[] = [
{
id: 'sensor_1',
name: 'Energy Monitor 1',
type: 'energy',
room: 'Conference Room A',
status: 'online',
lastSeen: Date.now() / 1000,
capabilities: {
monitoring: ['energy'],
actions: [],
},
metadata: {
location: 'Wall mounted',
model: 'EM-100',
firmware: '2.1.0',
},
},
{
id: 'sensor_2',
name: 'HVAC Controller 1',
type: 'hvac',
room: 'Conference Room A',
status: 'online',
lastSeen: Date.now() / 1000,
capabilities: {
monitoring: ['temperature', 'co2'],
actions: [
{
id: 'temp_adjust',
name: 'Adjust Temperature',
type: 'adjust',
icon: '🌡️',
parameters: { min: 18, max: 28, step: 0.5 },
},
{
id: 'fan_speed',
name: 'Fan Speed',
type: 'adjust',
icon: '💨',
parameters: { min: 0, max: 5, step: 1 },
},
{ id: 'power_toggle', name: 'Power', type: 'toggle', icon: '⚡' },
],
},
metadata: {
location: 'Ceiling mounted',
model: 'HVAC-200',
firmware: '3.2.1',
},
},
{
id: 'sensor_3',
name: 'Smart Light Controller',
type: 'lighting',
room: 'Office Floor 1',
status: 'online',
lastSeen: Date.now() / 1000,
capabilities: {
monitoring: ['energy'],
actions: [
{
id: 'brightness',
name: 'Brightness',
type: 'adjust',
icon: '💡',
parameters: { min: 0, max: 100, step: 5 },
},
{ id: 'power_toggle', name: 'Power', type: 'toggle', icon: '⚡' },
{
id: 'scene',
name: 'Scene',
type: 'adjust',
icon: '🎨',
parameters: { options: ['Work', 'Meeting', 'Presentation', 'Relax'] },
},
],
},
metadata: {
location: 'Ceiling grid',
model: 'SL-300',
firmware: '1.5.2',
},
},
{
id: 'sensor_4',
name: 'CO2 Sensor',
type: 'co2',
room: 'Meeting Room 1',
status: 'online',
lastSeen: Date.now() / 1000,
capabilities: {
monitoring: ['co2', 'temperature', 'humidity'],
actions: [{ id: 'calibrate', name: 'Calibrate', type: 'trigger', icon: '⚙️' }],
},
metadata: {
location: 'Wall mounted',
model: 'CO2-150',
firmware: '2.0.3',
battery: 85,
},
},
{
id: 'sensor_5',
name: 'Security Camera',
type: 'security',
room: 'Lobby',
status: 'online',
lastSeen: Date.now() / 1000,
capabilities: {
monitoring: ['motion'],
actions: [
{ id: 'record_toggle', name: 'Recording', type: 'toggle', icon: '📹' },
{ id: 'ptz_control', name: 'Pan/Tilt/Zoom', type: 'trigger', icon: '🎥' },
{ id: 'night_mode', name: 'Night Mode', type: 'toggle', icon: '🌙' },
],
},
metadata: {
location: 'Corner ceiling',
model: 'SEC-400',
firmware: '4.1.0',
},
},
]
mockSensors.forEach((sensor) => {
sensorDevices.set(sensor.id, sensor)
})
}
//initializeMockSensors()
// Load rooms from API on store initialization
loadRoomsFromAPI()
return {
// WebSocket state
isConnected,
latestMessage,
timeSeriesData,
sensorsData,
roomsData,
latestReadings,
sensorDevices,
availableRooms,
roomsLoading,
roomsLoaded,
// WebSocket state (delegated)
isConnected: computed(() => webSocketStore.isConnected),
latestMessage: computed(() => webSocketStore.latestMessage),
timeSeriesData: computed(() => webSocketStore.timeSeriesData),
// API state
apiSensors,
apiRooms,
analyticsData,
systemStatus,
healthStatus,
apiLoading,
apiError,
// Sensor state (delegated)
sensorsData: computed(() => sensorStore.sensorsData),
sensorDevices: computed(() => sensorStore.sensorDevices),
latestReadings: computed(() => sensorStore.latestReadings),
apiSensors: computed(() => Array.from(sensorStore.sensorDevices.values())), // Convert Map to Array
// 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,
disconnect,
getCO2Status,
updateSensorRoom,
executeSensorAction,
getSensorsByRoom,
getSensorsByType,
loadRoomsFromAPI,
addRoom,
removeRoom,
getRoomStats,
getAllRoomsWithStats,
// API functions
fetchApiSensors,
fetchApiSensorData,
updateApiSensorMetadata,
deleteApiSensor,
exportApiData,
fetchApiRooms,
fetchApiRoomData,
fetchAnalyticsSummary,
fetchEnergyTrends,
fetchRoomComparison,
fetchSystemEvents,
fetchSystemStatus,
fetchHealthStatus,
// Sensor functions (delegated)
updateSensorRoom: sensorStore.updateSensorRoom,
executeSensorAction: sensorStore.executeSensorAction,
getSensorsByRoom: sensorStore.getSensorsByRoom,
getSensorsByType: sensorStore.getSensorsByType,
fetchApiSensors: sensorStore.fetchApiSensors,
fetchApiSensorData: sensorStore.fetchApiSensorData,
updateApiSensorMetadata: sensorStore.updateApiSensorMetadata,
deleteApiSensor: sensorStore.deleteApiSensor,
exportApiData: sensorStore.exportApiData,
// Room functions (delegated)
getCO2Status: roomStore.getCO2Status,
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,
}
})

16
src/stores/index.ts Normal file
View 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
View 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
View 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
View 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,
}
})