|
@@ -0,0 +1,361 @@
|
|
|
|
|
+using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
+using Serilog;
|
|
|
|
|
+using YZWater.Core.Models;
|
|
|
|
|
+
|
|
|
|
|
+namespace YZWater.Core.Services;
|
|
|
|
|
+
|
|
|
|
|
+/// <summary>
|
|
|
|
|
+/// 报警服务 - 报警检测、持久化、确认、延迟
|
|
|
|
|
+/// </summary>
|
|
|
|
|
+public partial class AlarmService : ObservableObject, IDisposable
|
|
|
|
|
+{
|
|
|
|
|
+ private readonly PlcPollingService _pollingService;
|
|
|
|
|
+ private CancellationTokenSource? _cts;
|
|
|
|
|
+ private bool _disposed;
|
|
|
|
|
+
|
|
|
|
|
+ // ─── 报警配置 ───
|
|
|
|
|
+ public float LevelHighAlarm { get; set; } = 80f;
|
|
|
|
|
+ public float LevelLowAlarm { get; set; } = 20f;
|
|
|
|
|
+ public float FlowHighAlarm { get; set; } = 100f;
|
|
|
|
|
+ public float FlowLowAlarm { get; set; } = 5f;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 报警延迟(秒)- 持续超过此时间才触发
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public int AlarmDelaySeconds { get; set; } = 3;
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 报警死区(防止频繁触发)
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public float AlarmDeadband { get; set; } = 2f;
|
|
|
|
|
+
|
|
|
|
|
+ // ─── 状态 ───
|
|
|
|
|
+ [ObservableProperty] private bool _hasActiveAlarm;
|
|
|
|
|
+ [ObservableProperty] private string _activeAlarmMessage = string.Empty;
|
|
|
|
|
+ [ObservableProperty] private int _activeAlarmCount;
|
|
|
|
|
+
|
|
|
|
|
+ // ─── 事件 ───
|
|
|
|
|
+ public event Action<AlarmRecord>? AlarmRaised;
|
|
|
|
|
+ public event Action<AlarmRecord>? AlarmAcknowledged;
|
|
|
|
|
+ public event Action? AlarmStateChanged;
|
|
|
|
|
+
|
|
|
|
|
+ // ─── 报警跟踪 ───
|
|
|
|
|
+ private readonly Dictionary<string, DateTime> _alarmStartTimes = new();
|
|
|
|
|
+ private readonly HashSet<string> _activeAlarms = new();
|
|
|
|
|
+
|
|
|
|
|
+ public AlarmService(PlcPollingService pollingService)
|
|
|
|
|
+ {
|
|
|
|
|
+ _pollingService = pollingService;
|
|
|
|
|
+ _pollingService.DataUpdated += OnDataUpdated;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 启动报警监控
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public void Start()
|
|
|
|
|
+ {
|
|
|
|
|
+ _cts = new CancellationTokenSource();
|
|
|
|
|
+ Log.Information("报警服务已启动");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 停止报警监控
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public void Stop()
|
|
|
|
|
+ {
|
|
|
|
|
+ _cts?.Cancel();
|
|
|
|
|
+ Log.Information("报警服务已停止");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 数据更新回调
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void OnDataUpdated(PlcDataModel data)
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckAlarms(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 检查所有报警条件
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void CheckAlarms(PlcDataModel data)
|
|
|
|
|
+ {
|
|
|
|
|
+ var now = DateTime.Now;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查水箱液位
|
|
|
|
|
+ CheckLevelAlarm("Tank1", data.Tank1Level, now);
|
|
|
|
|
+ CheckLevelAlarm("Tank2", data.Tank2Level, now);
|
|
|
|
|
+ CheckLevelAlarm("Tank3", data.Tank3Level, now);
|
|
|
|
|
+ CheckLevelAlarm("Tank4", data.Tank4Level, now);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查流量
|
|
|
|
|
+ CheckFlowAlarm("Inflow", data.InflowRate, now);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查泵故障
|
|
|
|
|
+ CheckPumpFault("Pump1", data.Pump1Fault, now);
|
|
|
|
|
+ CheckPumpFault("Pump2", data.Pump2Fault, now);
|
|
|
|
|
+ CheckPumpFault("Pump3", data.Pump3Fault, now);
|
|
|
|
|
+ CheckPumpFault("Pump4", data.Pump4Fault, now);
|
|
|
|
|
+ CheckPumpFault("Pump5", data.Pump5Fault, now);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查风机故障
|
|
|
|
|
+ CheckFanFault("Fan1", data.Fan1Fault, now);
|
|
|
|
|
+ CheckFanFault("Fan2", data.Fan2Fault, now);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查 PLC 通信
|
|
|
|
|
+ if (!data.IsPlcConnected)
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckCondition("PLC_Connection", "PLC通信丢失", 1, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ClearCondition("PLC_Connection");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新状态
|
|
|
|
|
+ UpdateAlarmState();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 检查液位报警
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void CheckLevelAlarm(string tankId, float level, DateTime now)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (level > LevelHighAlarm + AlarmDeadband)
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckCondition($"{tankId}_High", $"{tankId} 液位过高 ({level:F1}%)", 2, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (level < LevelLowAlarm - AlarmDeadband)
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckCondition($"{tankId}_Low", $"{tankId} 液位过低 ({level:F1}%)", 2, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ClearCondition($"{tankId}_High");
|
|
|
|
|
+ ClearCondition($"{tankId}_Low");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 检查流量报警
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void CheckFlowAlarm(string flowId, float rate, DateTime now)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (rate > FlowHighAlarm)
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckCondition($"{flowId}_High", $"{flowId} 流量过高 ({rate:F1} m³/h)", 2, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (rate < FlowLowAlarm && rate > 0) // 0 表示无数据,不报警
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckCondition($"{flowId}_Low", $"{flowId} 流量过低 ({rate:F1} m³/h)", 2, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ClearCondition($"{flowId}_High");
|
|
|
|
|
+ ClearCondition($"{flowId}_Low");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 检查泵故障
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void CheckPumpFault(string pumpId, bool hasFault, DateTime now)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (hasFault)
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckCondition($"{pumpId}_Fault", $"{pumpId} 故障", 3, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ClearCondition($"{pumpId}_Fault");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 检查风机故障
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void CheckFanFault(string fanId, bool hasFault, DateTime now)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (hasFault)
|
|
|
|
|
+ {
|
|
|
|
|
+ CheckCondition($"{fanId}_Fault", $"{fanId} 故障", 3, now);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ClearCondition($"{fanId}_Fault");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 检查报警条件(带延迟)
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void CheckCondition(string conditionId, string message, int level, DateTime now)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_activeAlarms.Contains(conditionId))
|
|
|
|
|
+ return; // 已经在报警中
|
|
|
|
|
+
|
|
|
|
|
+ if (!_alarmStartTimes.ContainsKey(conditionId))
|
|
|
|
|
+ {
|
|
|
|
|
+ _alarmStartTimes[conditionId] = now;
|
|
|
|
|
+ return; // 记录开始时间
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查延迟
|
|
|
|
|
+ if ((now - _alarmStartTimes[conditionId]).TotalSeconds >= AlarmDelaySeconds)
|
|
|
|
|
+ {
|
|
|
|
|
+ TriggerAlarm(conditionId, message, level);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 清除报警条件
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void ClearCondition(string conditionId)
|
|
|
|
|
+ {
|
|
|
|
|
+ _alarmStartTimes.Remove(conditionId);
|
|
|
|
|
+ _activeAlarms.Remove(conditionId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 触发报警
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void TriggerAlarm(string conditionId, string message, int level)
|
|
|
|
|
+ {
|
|
|
|
|
+ _activeAlarms.Add(conditionId);
|
|
|
|
|
+ _alarmStartTimes.Remove(conditionId);
|
|
|
|
|
+
|
|
|
|
|
+ var alarm = new AlarmRecord
|
|
|
|
|
+ {
|
|
|
|
|
+ AlarmTime = DateTime.Now,
|
|
|
|
|
+ AlarmType = conditionId.Split('_')[0],
|
|
|
|
|
+ AlarmMessage = message,
|
|
|
|
|
+ AlarmLevel = level,
|
|
|
|
|
+ IsConfirmed = false
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 持久化到数据库
|
|
|
|
|
+ _ = SaveAlarmAsync(alarm);
|
|
|
|
|
+
|
|
|
|
|
+ // 通知
|
|
|
|
|
+ AlarmRaised?.Invoke(alarm);
|
|
|
|
|
+ UpdateAlarmState();
|
|
|
|
|
+
|
|
|
|
|
+ Log.Warning("报警触发: {Message} (级别 {Level})", message, level);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 保存报警到数据库
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private async Task SaveAlarmAsync(AlarmRecord alarm)
|
|
|
|
|
+ {
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ await DatabaseService.Db.Insert(alarm).ExecuteAffrowsAsync();
|
|
|
|
|
+ Log.Debug("报警记录已保存: {Id}", alarm.Id);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ Log.Error(ex, "保存报警记录失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 确认报警
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task AcknowledgeAlarmAsync(int alarmId, string operatorName = "操作员")
|
|
|
|
|
+ {
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ await DatabaseService.Db.Update<AlarmRecord>()
|
|
|
|
|
+ .Set(r => r.IsConfirmed, true)
|
|
|
|
|
+ .Set(r => r.ConfirmedTime, DateTime.Now)
|
|
|
|
|
+ .Set(r => r.ConfirmedBy, operatorName)
|
|
|
|
|
+ .Where(r => r.Id == alarmId)
|
|
|
|
|
+ .ExecuteAffrowsAsync();
|
|
|
|
|
+
|
|
|
|
|
+ var alarm = await DatabaseService.Db.Select<AlarmRecord>()
|
|
|
|
|
+ .Where(r => r.Id == alarmId)
|
|
|
|
|
+ .FirstAsync();
|
|
|
|
|
+
|
|
|
|
|
+ if (alarm != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ AlarmAcknowledged?.Invoke(alarm);
|
|
|
|
|
+ UpdateAlarmState();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log.Information("报警 {Id} 已确认", alarmId);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ Log.Error(ex, "确认报警失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 批量确认所有未确认报警
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task AcknowledgeAllAsync(string operatorName = "操作员")
|
|
|
|
|
+ {
|
|
|
|
|
+ try
|
|
|
|
|
+ {
|
|
|
|
|
+ await DatabaseService.Db.Update<AlarmRecord>()
|
|
|
|
|
+ .Set(r => r.IsConfirmed, true)
|
|
|
|
|
+ .Set(r => r.ConfirmedTime, DateTime.Now)
|
|
|
|
|
+ .Set(r => r.ConfirmedBy, operatorName)
|
|
|
|
|
+ .Where(r => !r.IsConfirmed)
|
|
|
|
|
+ .ExecuteAffrowsAsync();
|
|
|
|
|
+
|
|
|
|
|
+ UpdateAlarmState();
|
|
|
|
|
+ Log.Information("所有报警已确认");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (Exception ex)
|
|
|
|
|
+ {
|
|
|
|
|
+ Log.Error(ex, "批量确认报警失败");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 更新报警状态
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ private void UpdateAlarmState()
|
|
|
|
|
+ {
|
|
|
|
|
+ HasActiveAlarm = _activeAlarms.Count > 0;
|
|
|
|
|
+ ActiveAlarmCount = _activeAlarms.Count;
|
|
|
|
|
+ ActiveAlarmMessage = _activeAlarms.Count > 0
|
|
|
|
|
+ ? string.Join("; ", _activeAlarms.Take(3))
|
|
|
|
|
+ : string.Empty;
|
|
|
|
|
+
|
|
|
|
|
+ AlarmStateChanged?.Invoke();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 从数据库加载未确认报警
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task<List<AlarmRecord>> GetUnacknowledgedAlarmsAsync()
|
|
|
|
|
+ {
|
|
|
|
|
+ return await DatabaseService.Db.Select<AlarmRecord>()
|
|
|
|
|
+ .Where(r => !r.IsConfirmed)
|
|
|
|
|
+ .OrderByDescending(r => r.AlarmTime)
|
|
|
|
|
+ .ToListAsync();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// 获取报警历史
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ public async Task<List<AlarmRecord>> GetAlarmHistoryAsync(DateTime start, DateTime end)
|
|
|
|
|
+ {
|
|
|
|
|
+ return await DatabaseService.Db.Select<AlarmRecord>()
|
|
|
|
|
+ .Where(r => r.AlarmTime >= start && r.AlarmTime <= end)
|
|
|
|
|
+ .OrderByDescending(r => r.AlarmTime)
|
|
|
|
|
+ .ToListAsync();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void Dispose()
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_disposed) return;
|
|
|
|
|
+ _disposed = true;
|
|
|
|
|
+ _pollingService.DataUpdated -= OnDataUpdated;
|
|
|
|
|
+ Stop();
|
|
|
|
|
+ Log.Information("报警服务已释放");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|