207 lines
No EOL
5.9 KiB
TypeScript
207 lines
No EOL
5.9 KiB
TypeScript
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<string, AlarmEvent> = 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);
|
|
}
|
|
}
|
|
} |