- Added @elysiajs/swagger dependency to package.json for API documentation. - Removed the old bed router and replaced it with a new history router. - Created a new state router to manage WebSocket connections and state updates. - Implemented a comprehensive state management system with the StateManager service. - Introduced AlarmManagement and BedService services for handling alarms and sensor readings. - Established a new MQTT service for managing MQTT connections and subscriptions. - Created an AlarmStateStore to manage volatile alerts and their states. - Defined FrontendState types for structured state management and WebSocket messaging.
232 lines
No EOL
7.3 KiB
TypeScript
232 lines
No EOL
7.3 KiB
TypeScript
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<FrontendState> {
|
|
return { ...this.state };
|
|
}
|
|
|
|
// Initialize state from database
|
|
async initializeState(): Promise<void> {
|
|
try { // Load measurement points
|
|
const measurementPoints = await this.bedService.getMeasurementPoints();
|
|
const measurementPointStates: Record<string, MeasurementPointState> = {};
|
|
|
|
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<string, AlertState> = {};
|
|
|
|
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<SystemStatus>): 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
await this.prisma.$disconnect();
|
|
this.removeAllListeners();
|
|
}
|
|
} |