449 lines
13 KiB
TypeScript
449 lines
13 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, reactive } from 'vue'
|
|
|
|
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', () => {
|
|
// 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[]>([
|
|
'Conference Room A',
|
|
'Conference Room B',
|
|
'Office Floor 1',
|
|
'Office Floor 2',
|
|
'Kitchen',
|
|
'Lobby',
|
|
'Server Room',
|
|
'Storage Room',
|
|
'Meeting Room 1',
|
|
'Meeting Room 2'
|
|
])
|
|
|
|
let socket: WebSocket | null = null
|
|
const newDataBuffer: (LegacyEnergyData | SensorReading)[] = []
|
|
|
|
// Actions
|
|
function connect(url: string) {
|
|
if (isConnected.value) {
|
|
console.log('Already connected.')
|
|
return
|
|
}
|
|
|
|
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)
|
|
newDataBuffer.push(data)
|
|
} catch (error) {
|
|
console.error('Error parsing incoming data:', error)
|
|
}
|
|
}
|
|
|
|
socket.onclose = () => {
|
|
console.log('WebSocket connection closed.')
|
|
isConnected.value = false
|
|
socket = null
|
|
}
|
|
|
|
socket.onerror = (error) => {
|
|
console.error('WebSocket error:', error)
|
|
isConnected.value = false
|
|
socket = null
|
|
}
|
|
|
|
// Process the buffer at intervals
|
|
setInterval(() => {
|
|
if (newDataBuffer.length > 0) {
|
|
const data = newDataBuffer.shift() // Get the oldest data point
|
|
if (data) {
|
|
// 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'
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// Initialize mock sensors on store creation
|
|
initializeMockSensors()
|
|
|
|
return {
|
|
isConnected,
|
|
latestMessage,
|
|
timeSeriesData,
|
|
sensorsData,
|
|
roomsData,
|
|
latestReadings,
|
|
sensorDevices,
|
|
availableRooms,
|
|
connect,
|
|
disconnect,
|
|
getCO2Status,
|
|
updateSensorRoom,
|
|
executeSensorAction,
|
|
getSensorsByRoom,
|
|
getSensorsByType
|
|
}
|
|
})
|