import { EventEmitter } from 'events'; import { FrontendState, MeasurementPointState, AlertState, SystemStatus, StateUpdateEvent } from '../types/FrontendState'; import { BedService } from './BedService'; import { VolatileAlert } from '../store/AlarmStateStore'; import { PrismaClient } from '../generated/prisma'; export class StateManager extends EventEmitter { private state: FrontendState; private prisma: PrismaClient; constructor(private bedService: BedService) { super(); this.prisma = new PrismaClient(); // Initialize empty state this.state = { measurementPoints: {}, alerts: {}, system: { mqttConnected: false, databaseConnected: false, lastHeartbeat: new Date(), activeConnections: 0, totalMeasurementPoints: 0, activeSensors: 0 } }; this.setupEventListeners(); } private setupEventListeners(): void { // Listen to BedService events this.bedService.on('sensorReading', (reading) => { this.updateSensorReading(reading.sensorId, reading.value, reading.timestamp); }); this.bedService.on('alert', (alert) => { this.updateAlert(alert); }); this.bedService.on('initialized', () => { this.updateSystemStatus({ mqttConnected: true }); }); this.bedService.on('disconnected', () => { this.updateSystemStatus({ mqttConnected: false }); }); } // Get current state (read-only) getState(): Readonly { return { ...this.state }; } // Initialize state from database async initializeState(): Promise { try { // Load measurement points const measurementPoints = await this.bedService.getMeasurementPoints(); const measurementPointStates: Record = {}; for (const mp of measurementPoints) { measurementPointStates[mp.id] = { id: mp.id, sensorId: mp.sensorId, label: mp.label, zone: mp.zone, x: mp.x ?? 0, y: mp.y ?? 0, pin: mp.pin ?? 0, currentValue: 0, lastUpdateTime: new Date(), warningThreshold: mp.warningThreshold, alarmThreshold: mp.alarmThreshold, warningDelayMs: mp.warningDelayMs, status: 'offline' }; } // Load active alerts const alerts = await this.bedService.getActiveAlerts(); const alertStates: Record = {}; for (const alert of alerts) { const measurementPoint = measurementPointStates[alert.measurementPointId]; alertStates[alert.id] = { id: alert.id, measurementPointId: alert.measurementPointId, type: alert.type, value: alert.value, threshold: alert.threshold, acknowledged: alert.acknowledged, silenced: alert.silenced, startTime: alert.startTime, endTime: alert.endTime ?? undefined, sensorLabel: measurementPoint?.label || 'Unknown', zone: measurementPoint?.zone || 'Unknown' }; } // Update state this.state.measurementPoints = measurementPointStates; this.state.alerts = alertStates; this.state.system.totalMeasurementPoints = measurementPoints.length; this.state.system.databaseConnected = true; this.emitStateUpdate('FULL_STATE', this.state); } catch (error) { console.error('Failed to initialize state:', error); this.state.system.databaseConnected = false; } } // Update sensor reading updateSensorReading(sensorId: string, value: number, timestamp: Date): void { // Find measurement point by sensorId const measurementPoint = Object.values(this.state.measurementPoints) .find(mp => mp.sensorId === sensorId); if (!measurementPoint) return; // Determine status based on thresholds let status: 'normal' | 'warning' | 'alarm' | 'offline' = 'normal'; if (value >= measurementPoint.alarmThreshold) { status = 'alarm'; } else if (value >= measurementPoint.warningThreshold) { status = 'warning'; } // Update measurement point state this.state.measurementPoints[measurementPoint.id] = { ...measurementPoint, currentValue: value, lastUpdateTime: timestamp, status }; // Update system stats this.updateActiveSensors(); this.emitStateUpdate('SENSOR_UPDATE', this.state.measurementPoints[measurementPoint.id]); } // Update alert updateAlert(alert: Alert & { measurementPoint: MeasurementPoint }): void { const alertState: AlertState = { id: alert.id, measurementPointId: alert.measurementPointId, type: alert.type, value: alert.value, threshold: alert.threshold, acknowledged: alert.acknowledged, silenced: alert.silenced, startTime: alert.startTime, endTime: alert.endTime || undefined, sensorLabel: alert.measurementPoint.label, zone: alert.measurementPoint.zone }; this.state.alerts[alert.id] = alertState; this.emitStateUpdate('ALERT_UPDATE', alertState); } // Remove alert (when closed) removeAlert(alertId: string): void { if (this.state.alerts[alertId]) { delete this.state.alerts[alertId]; this.emitStateUpdate('PARTIAL_UPDATE', { alerts: this.state.alerts }); } } // Update system status updateSystemStatus(updates: Partial): void { this.state.system = { ...this.state.system, ...updates, lastHeartbeat: new Date() }; this.emitStateUpdate('SYSTEM_UPDATE', this.state.system); } // Update active sensors count private updateActiveSensors(): void { const now = new Date(); const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); const activeSensors = Object.values(this.state.measurementPoints) .filter(mp => mp.lastUpdateTime > fiveMinutesAgo).length; this.state.system.activeSensors = activeSensors; } // Update connection count (for WebSocket clients) updateConnectionCount(count: number): void { this.state.system.activeConnections = count; this.emitStateUpdate('SYSTEM_UPDATE', this.state.system); } // Acknowledge alert async acknowledgeAlert(alertId: string): Promise { try { await this.bedService.acknowledgeAlert(alertId); if (this.state.alerts[alertId]) { this.state.alerts[alertId].acknowledged = true; this.emitStateUpdate('ALERT_UPDATE', this.state.alerts[alertId]); } } catch (error) { console.error('Failed to acknowledge alert:', error); } } // Silence alert async silenceAlert(alertId: string): Promise { try { await this.bedService.silenceAlert(alertId); if (this.state.alerts[alertId]) { this.state.alerts[alertId].silenced = true; this.emitStateUpdate('ALERT_UPDATE', this.state.alerts[alertId]); } } catch (error) { console.error('Failed to silence alert:', error); } } // Emit state update event private emitStateUpdate(type: StateUpdateEvent['type'], data: StateUpdateEvent['data']): void { const event: StateUpdateEvent = { type, timestamp: new Date(), data }; this.emit('stateUpdate', event); } // Cleanup async disconnect(): Promise { await this.prisma.$disconnect(); this.removeAllListeners(); } }