//******************************************************************************************************
// AlarmAdapter.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 01/31/2012 - Stephen C. Wills
// Generated original version of source code.
// 12/13/2012 - Starlynn Danyelle Gilliam
// Modified Header.
// 02/02/2021 - C. Lackner
// Added Settings for Alarm Retention
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading;
using DataQualityMonitoring.Services;
using GSF;
using GSF.Collections;
using GSF.Data;
using GSF.Diagnostics;
using GSF.Threading;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
using GSF.TimeSeries.Statistics;
namespace DataQualityMonitoring
{
///
/// Action adapter that generates alarm measurements based on alarm definitions from the database.
///
[Description("Alarm: Generates alarm events for alarms defined in the database")]
public class AlarmAdapter : FacileActionAdapterBase
{
#region [ Members ]
// Nested Types
private class SignalAlarms
{
public string Name;
public List Alarms;
public AlarmStatistics Statistics;
}
private class StateChange
{
public Guid SignalID;
public int? OldStateID;
public int? NewStateID;
public DateTime Timestamp;
public double Value;
}
// Constants
private const int UpToDate = 0;
private const int Modified = 1;
private const int DefaultAlarmRetention = 24;
// Fields
private readonly object m_alarmLock;
private readonly DoubleBufferedQueue m_measurementQueue;
private readonly MixedSynchronizedOperation m_processMeasurementsOperation;
private DataSet m_alarmDataSet;
private int m_dataSourceState;
private Dictionary m_alarmLookup;
private AlarmService m_alarmService;
private long m_eventCount;
private readonly LongSynchronizedOperation m_alarmLogOperation;
private readonly DoubleBufferedQueue m_stateChanges;
private bool m_useAlarmLog;
private int m_bulkInsertLimit;
private int m_logProcessingDelay;
private bool m_supportsTemporalProcessing;
private bool m_disposed;
private int m_alarmRetention;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
public AlarmAdapter()
{
m_alarmLock = new object();
m_alarmLookup = new Dictionary();
m_measurementQueue = new DoubleBufferedQueue();
m_processMeasurementsOperation = new MixedSynchronizedOperation(ProcessMeasurements, ex => OnProcessException(MessageLevel.Warning, ex));
m_alarmLogOperation = new LongSynchronizedOperation(LogStateChanges, ex => OnProcessException(MessageLevel.Warning, ex));
m_stateChanges = new DoubleBufferedQueue();
m_alarmLogOperation.IsBackground = true;
Default = this;
}
#endregion
#region [ Properties ]
///
/// Gets or sets based data source available to this .
///
public override DataSet DataSource
{
get
{
return base.DataSource;
}
set
{
base.DataSource = value;
if (Interlocked.CompareExchange(ref m_dataSourceState, Modified, UpToDate) == UpToDate && Initialized)
{
m_processMeasurementsOperation.AsynchronousExecutionMode = AsynchronousExecutionMode.Long;
m_processMeasurementsOperation.RunOnceAsync();
}
}
}
///
/// Gets the flag indicating if this adapter supports temporal processing.
///
[ConnectionStringParameter,
Description("Define the flag indicating if this adapter supports temporal processing."),
DefaultValue(false)]
public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing;
///
/// Gets or sets the flag indicating whether the alarm adapter should
/// use the alarm log to track recently modified alarm states.
///
[ConnectionStringParameter,
Description("Define the flag indicating whether to use the alarm log to track recently modified alarm states."),
DefaultValue(false)]
public bool UseAlarmLog
{
get
{
return m_useAlarmLog;
}
set
{
m_useAlarmLog = value;
}
}
///
/// Gets or sets the maximum number of alarm state changes to pack into one alarm log insert query.
///
[ConnectionStringParameter,
Description("Define the maximum number of alarm state changes to pack into one alarm log insert query."),
DefaultValue(300)]
public int BulkInsertLimit
{
get
{
return m_bulkInsertLimit;
}
set
{
m_bulkInsertLimit = value;
}
}
///
/// Gets or sets the amount of time, in seconds, to wait for alarm log state changes between bulk inserts.
///
[ConnectionStringParameter,
Description("Define the amount of time, in seconds, to wait for alarm log state changes between bulk inserts."),
DefaultValue(1.0D)]
public double LogProcessingDelay
{
get
{
return m_logProcessingDelay / 1000.0D;
}
set
{
m_logProcessingDelay = (int)(value * 1000.0D);
}
}
///
/// Gets or sets the amount of time, in hours, an Alarm change will be kept n the database.
///
[ConnectionStringParameter,
Description("Define the amount of time, in hour, the alarms will be kept in the AlarmTable."),
DefaultValue(false)]
public int AlarmRetention
{
get
{
return m_alarmRetention;
}
set
{
m_alarmRetention = value;
}
}
///
/// Returns the detailed status of the data input source.
///
public override string Status
{
get
{
StringBuilder statusBuilder = new StringBuilder(base.Status);
statusBuilder.Append(m_alarmService.Status);
return statusBuilder.ToString();
}
}
#endregion
#region [ Methods ]
///
/// Initializes the .
///
public override void Initialize()
{
Dictionary settings;
string setting;
double logProcessingDelay;
// Run base class initialization
base.Initialize();
settings = Settings;
// Load optional parameters
if (settings.TryGetValue("supportsTemporalProcessing", out setting))
m_supportsTemporalProcessing = setting.ParseBoolean();
else
m_supportsTemporalProcessing = false;
if (settings.TryGetValue("useAlarmLog", out setting))
m_useAlarmLog = setting.ParseBoolean();
else
m_useAlarmLog = false;
if (!settings.TryGetValue("bulkInsertLimit", out setting) || !int.TryParse(setting, out m_bulkInsertLimit))
m_bulkInsertLimit = 300;
if (!settings.TryGetValue("alarmRetention", out setting) || !int.TryParse(setting, out m_alarmRetention))
m_alarmRetention = DefaultAlarmRetention;
if (settings.TryGetValue("logProcessingDelay", out setting) && double.TryParse(setting, out logProcessingDelay))
m_logProcessingDelay = (int)(logProcessingDelay * 1000.0D);
else
m_logProcessingDelay = 1000;
try
{
// Set up alarm service
m_alarmService = new AlarmService(this);
m_alarmService.SettingsCategory = base.Name.Replace("!", "").ToLower() + m_alarmService.SettingsCategory;
m_alarmService.ServiceProcessException += AlarmService_ServiceProcessException;
m_alarmService.PersistSettings = true;
m_alarmService.Initialize();
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Error, new InvalidOperationException($"Unable to initialize alarm service due to exception: {ex.Message}", ex));
}
// Run the process measurements operation to ensure that the alarm configuration is up to date
if (Interlocked.CompareExchange(ref m_dataSourceState, Modified, Modified) == Modified)
{
m_processMeasurementsOperation.AsynchronousExecutionMode = AsynchronousExecutionMode.Long;
m_processMeasurementsOperation.RunOnceAsync();
}
}
///
/// Starts the , or restarts it if it is already running.
///
public override void Start()
{
base.Start();
Interlocked.Exchange(ref m_eventCount, 0L);
}
///
/// Queues a collection of measurements for processing.
///
/// Measurements to queue for processing.
public override void QueueMeasurementsForProcessing(IEnumerable measurements)
{
m_measurementQueue.Enqueue(measurements);
m_processMeasurementsOperation.RunOnceAsync();
}
///
/// Gets a short one-line status of this .
///
/// Maximum number of available characters for display.
/// A short one-line summary of the current status of this .
public override string GetShortStatus(int maxLength)
{
return $"{Interlocked.Read(ref m_eventCount)} events processed since last start".CenterText(maxLength);
}
///
/// Gets a collection containing all the raised alarms in the system.
///
/// A collection containing all the raised alarms.
[AdapterCommand("Gets a collection containing all the raised alarms in the system.", "Administrator", "Editor", "Viewer")]
public ICollection GetRaisedAlarms()
{
lock (m_alarmLock)
{
return m_alarmLookup
.SelectMany(kvp => kvp.Value.Alarms)
.Where(alarm => alarm.State == AlarmState.Raised)
.Select(alarm => alarm.Clone())
.ToList();
}
}
///
/// Gets a collection containing raised alarms with the highest severity for each signal in the system.
///
/// A collection containing all the highest severity raised alarms.
[AdapterCommand("Gets a collection containing raised alarms with the highest severity for each signal in the system.", "Administrator", "Editor", "Viewer")]
public ICollection GetHighestSeverityAlarms()
{
lock (m_alarmLock)
{
return m_alarmLookup
.Select(kvp => kvp.Value.Alarms.FirstOrDefault(alarm => alarm.State == AlarmState.Raised))
.Where(alarm => (object)alarm != null)
.Select(alarm => alarm.Clone())
.ToList();
}
}
///
/// Gets Status of an Alarm Based on PointID.
///
/// PointID of the point the alarm is for.
/// A List of object including the state for a specific PointID.
public ICollection GetAlarmStatus(Guid PointID)
{
lock (m_alarmLock)
{
if (m_alarmLookup.ContainsKey(PointID))
return m_alarmLookup
.Where(kvp => kvp.Key == PointID).Select(kvp => kvp.Value.Alarms)
.Where(alarm => (object)alarm != null).First()
.Select(alarm => alarm.Clone())
.ToList();
else
return new List();
}
}
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
{
if (m_alarmService != null)
{
m_alarmService.ServiceProcessException -= AlarmService_ServiceProcessException;
m_alarmService.Dispose();
}
lock (m_alarmLock)
{
foreach (SignalAlarms signalAlarms in m_alarmLookup.Values)
StatisticsEngine.Unregister(signalAlarms.Statistics);
}
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
}
// Creates an alarm using data defined in the database.
private Alarm CreateAlarm(DataRow row)
{
object associatedMeasurementId = row.Field