Files
sa4cps-frontend/src/stores/room.ts
rafaeldpsilva b9348140b8 Refactor to decouple energy, room, and sensor stores
- Remove room and sensor logic from energy store - Update components to
use useRoomStore and useSensorStore directly - Fix sensor/room ID
mismatches and API response handling in room store - Update
AIOptimizationView to use useWebSocketStore for connection status -
Update SensorManagementView to use useRoomStore and useSensorStore
directly
2025-10-01 12:26:44 +01:00

290 lines
8.7 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
import { roomsApi, type RoomInfo as ApiRoomInfo, type SensorReading } 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
}
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()
// Validate data structure and provide fallbacks
if (!data.energy || !data.co2) {
console.warn('Invalid sensor reading data, missing energy or co2 properties:', data)
return
}
// 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.sensor_id],
energy: { current: 0, total: 0, average: 0, unit: data.energy?.unit || 'kWh' },
co2: { current: 0, average: 0, max: 0, status: 'good', unit: data.co2?.unit || 'ppm' },
occupancyEstimate: 'low',
lastUpdated: data.timestamp,
}
roomsData.set(data.room, roomMetrics)
}
// Update room sensors list
if (!roomMetrics.sensors.includes(data.sensor_id)) {
roomMetrics.sensors.push(data.sensor_id)
}
// 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.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) {
// Handle both response formats: {rooms: [...]} or direct array [...]
const roomsArray = Array.isArray(result) ? result : result.rooms || []
apiRooms.value = roomsArray
// Update available rooms from API data
const roomNames = roomsArray.map((room) => room.name || 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,
}
})