|
|
@@ -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("鎶ヨ鏈嶅姟宸查噴鏀");
|
|
|
+ }
|
|
|
+}
|