export interface AlarmEvent { id: string; sensorId: string; sensorLabel: string; type: 'warning' | 'alarm'; value: number; threshold: number; timestamp: number; time: string; acknowledged: boolean; silenced: boolean; silencedUntil?: number; } export class AlarmManager { private static instance: AlarmManager; private activeAlarms: Map = new Map(); private alarmHistory: AlarmEvent[] = []; private alarmCallbacks: ((alarm: AlarmEvent) => void)[] = []; private audioContext: AudioContext | null = null; private alarmSound: AudioBuffer | null = null; private isPlayingAlarm = false; private constructor() { this.initializeAudio(); } static getInstance(): AlarmManager { if (!AlarmManager.instance) { AlarmManager.instance = new AlarmManager(); } return AlarmManager.instance; } private async initializeAudio() { try { // Check if we're in browser environment if (typeof window !== 'undefined') { this.audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)(); // Generate alarm sound programmatically await this.generateAlarmSound(); } } catch (error) { console.warn('Audio context not available:', error); } } private async generateAlarmSound() { if (!this.audioContext) return; const sampleRate = this.audioContext.sampleRate; const duration = 1; // 1 second const buffer = this.audioContext.createBuffer(1, sampleRate * duration, sampleRate); const data = buffer.getChannelData(0); // Generate a beeping sound (sine wave with modulation) for (let i = 0; i < data.length; i++) { const t = i / sampleRate; const frequency = 800; // Hz const envelope = Math.sin(t * Math.PI * 4) > 0 ? 1 : 0; // Beeping pattern data[i] = Math.sin(2 * Math.PI * frequency * t) * envelope * 0.3; } this.alarmSound = buffer; } private async playAlarmSound() { if (!this.audioContext || !this.alarmSound || this.isPlayingAlarm) return; try { // Resume audio context if suspended if (this.audioContext.state === 'suspended') { await this.audioContext.resume(); } const source = this.audioContext.createBufferSource(); source.buffer = this.alarmSound; source.connect(this.audioContext.destination); source.start(); this.isPlayingAlarm = true; source.onended = () => { this.isPlayingAlarm = false; }; } catch (error) { console.warn('Failed to play alarm sound:', error); } } addAlarm(sensorId: string, sensorLabel: string, type: 'warning' | 'alarm', value: number, threshold: number) { const alarmId = `${sensorId}-${type}`; const timestamp = Date.now(); const alarm: AlarmEvent = { id: alarmId, sensorId, sensorLabel, type, value, threshold, timestamp, time: new Date(timestamp).toLocaleTimeString(), acknowledged: false, silenced: false }; // Check if alarm is already active and silenced const existingAlarm = this.activeAlarms.get(alarmId); if (existingAlarm?.silenced && existingAlarm.silencedUntil && timestamp < existingAlarm.silencedUntil) { // Update values but keep silenced state alarm.silenced = true; alarm.silencedUntil = existingAlarm.silencedUntil; } this.activeAlarms.set(alarmId, alarm); this.alarmHistory.unshift(alarm); // Keep only last 100 history items if (this.alarmHistory.length > 100) { this.alarmHistory = this.alarmHistory.slice(0, 100); } // Play sound for new alarms if (type === 'alarm' && !alarm.silenced) { this.playAlarmSound(); } // Notify callbacks this.alarmCallbacks.forEach(callback => callback(alarm)); return alarm; } clearAlarm(sensorId: string, type?: 'warning' | 'alarm') { if (type) { const alarmId = `${sensorId}-${type}`; this.activeAlarms.delete(alarmId); } else { // Clear all alarms for this sensor this.activeAlarms.delete(`${sensorId}-warning`); this.activeAlarms.delete(`${sensorId}-alarm`); } } acknowledgeAlarm(alarmId: string) { const alarm = this.activeAlarms.get(alarmId); if (alarm) { alarm.acknowledged = true; this.activeAlarms.set(alarmId, alarm); } } silenceAlarm(alarmId: string, durationMs: number = 300000) { // Default 5 minutes const alarm = this.activeAlarms.get(alarmId); if (alarm) { alarm.silenced = true; alarm.silencedUntil = Date.now() + durationMs; this.activeAlarms.set(alarmId, alarm); } } silenceAllAlarms(durationMs: number = 300000) { const timestamp = Date.now(); this.activeAlarms.forEach((alarm, alarmId) => { alarm.silenced = true; alarm.silencedUntil = timestamp + durationMs; this.activeAlarms.set(alarmId, alarm); }); } getActiveAlarms(): AlarmEvent[] { const now = Date.now(); const activeAlarms: AlarmEvent[] = []; this.activeAlarms.forEach((alarm, alarmId) => { // Check if silence period has expired if (alarm.silenced && alarm.silencedUntil && now >= alarm.silencedUntil) { alarm.silenced = false; alarm.silencedUntil = undefined; this.activeAlarms.set(alarmId, alarm); } activeAlarms.push(alarm); }); return activeAlarms.sort((a, b) => b.timestamp - a.timestamp); } getAlarmHistory(): AlarmEvent[] { return this.alarmHistory; } hasUnacknowledgedAlarms(): boolean { return Array.from(this.activeAlarms.values()).some(alarm => !alarm.acknowledged); } onAlarm(callback: (alarm: AlarmEvent) => void) { this.alarmCallbacks.push(callback); } offAlarm(callback: (alarm: AlarmEvent) => void) { const index = this.alarmCallbacks.indexOf(callback); if (index > -1) { this.alarmCallbacks.splice(index, 1); } } }