//****************************************************************************************************** // FlatlineTest.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: // ---------------------------------------------------------------------------------------------------- // 11/10/2009 - Stephen C. Wills // Generated original version of source code. // 12/11/2009 - Pinal C. Patel // Added thread synchronization. // Replaced the use of LatestTimestamp with base.RealTime. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Text; using System.Timers; using DataQualityMonitoring.Services; using GSF; using GSF.Diagnostics; using GSF.Net.Smtp; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; namespace DataQualityMonitoring { /// /// Tests measurements to determine whether they have flat-lined. /// [Description("Flat-line Test: Notifies when measurements are received whose values do not change")] public class FlatlineTest : ActionAdapterBase { #region [ Members ] // Fields private Ticks m_minFlatline; private Ticks m_warnInterval; private Ticks m_emailInterval; private string m_adminEmailAddress; private string m_smtpServer; private readonly Dictionary m_lastChange; private readonly Dictionary m_lastNotified; private readonly Timer m_warningTimer; private FlatlineService m_flatlineService; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// public FlatlineTest() { m_minFlatline = Ticks.FromSeconds(4); m_warnInterval = Ticks.FromSeconds(4); m_emailInterval = Ticks.FromSeconds(3600); m_smtpServer = Mail.DefaultSmtpServer; m_lastChange = new Dictionary(); m_lastNotified = new Dictionary(); m_warningTimer = new Timer(); } #endregion #region [ Properties ] /// /// Gets or sets the length of time, in seconds, that a measurement's value must remain stale to be considered flat-lined. /// [ConnectionStringParameter, Description("Define the length of time, in seconds, that a measurement's value must remain stale to be considered flat-lined."), DefaultValue(4)] public double MinFlatline { get { return m_minFlatline.ToSeconds(); } set { m_minFlatline = Ticks.FromSeconds(value); } } /// /// Gets or sets the amount of time, in seconds, between console updates. /// [ConnectionStringParameter, Description("Define the amount of time, in seconds, between console updates."), DefaultValue(4)] public double WarnInterval { get { return m_warnInterval.ToSeconds(); } set { m_warnInterval = Ticks.FromSeconds(value); } } /// /// Gets or sets the email address or address list to which notifications will be sent. /// [ConnectionStringParameter, Description("Define the email address or address list to which notifications will be sent."), DefaultValue(null)] public string AdminEmailAddress { get { return m_adminEmailAddress; } set { m_adminEmailAddress = value; } } /// /// Gets or sets the amount of time, in minutes, between email notifications. /// [ConnectionStringParameter, Description("Define the amount of time, in minutes, between email notifications."), DefaultValue(60)] public long EmailInterval { get { return (long)m_emailInterval.ToSeconds() / 60L; } set { m_emailInterval = Ticks.FromSeconds(value * 60L); } } /// /// Gets or sets the host name or IP address of the SMTP server used to send email notifications. /// [ConnectionStringParameter, Description("Define the host name or IP address of the SMTP server used to send email notifications."), DefaultValue(Mail.DefaultSmtpServer)] public string SmtpServer { get { return m_smtpServer; } set { m_smtpServer = value; } } /// /// Gets the flag indicating if this adapter supports temporal processing. /// public override bool SupportsTemporalProcessing => false; #endregion #region [ Methods ] /// /// Initializes . /// public override void Initialize() { base.Initialize(); Dictionary settings = Settings; string setting; if (settings.TryGetValue("minFlatline", out setting)) m_minFlatline = Ticks.FromSeconds(double.Parse(setting)); if (settings.TryGetValue("warnInterval", out setting)) m_warnInterval = Ticks.FromSeconds(double.Parse(setting)); if (settings.TryGetValue("adminEmailAddress", out m_adminEmailAddress)) { // emailInterval is entered in minutes. if (settings.TryGetValue("emailInterval", out setting)) m_emailInterval = Ticks.FromSeconds(long.Parse(setting) * 60L); } if (settings.TryGetValue("smtpServer", out setting)) m_smtpServer = setting; m_warningTimer.Interval = m_warnInterval.ToMilliseconds(); m_warningTimer.Elapsed += m_warningTimer_Elapsed; m_flatlineService = new FlatlineService(this); m_flatlineService.SettingsCategory = base.Name + m_flatlineService.SettingsCategory; m_flatlineService.ServiceProcessException += m_flatlineService_ServiceProcessException; m_flatlineService.Initialize(); InitializeLastChange(); } /// /// Starts the , if it is not already running. /// public override void Start() { base.Start(); m_warningTimer.Start(); } /// /// Stops the . /// public override void Stop() { m_warningTimer.Stop(); base.Stop(); } /// /// Publish of time-aligned collection of values that arrived within the /// concentrator's defined . /// /// of measurements with the same timestamp that arrived within that are ready for processing. /// Index of within a second ranging from zero to - 1. protected override void PublishFrame(IFrame frame, int index) { IMeasurement measurement; lock (m_lastChange) { foreach (MeasurementKey key in frame.Measurements.Keys) { measurement = frame.Measurements[key]; if (!m_lastChange.ContainsKey(key)) m_lastChange.Add(key, measurement); else if (m_lastChange[key].Value != measurement.Value) m_lastChange[key] = measurement; } } SendEmailNotifications(); } /// /// Returns a collection of measurements that are flat-lined. /// /// A collection of flat-lined measurements. public ICollection GetFlatlinedMeasurements() { ICollection flatlinedMeasurements = new List(); lock (m_lastChange) { IMeasurement measurement; foreach (MeasurementKey key in m_lastChange.Keys) { measurement = m_lastChange[key]; Ticks timeDiff = base.RealTime - measurement.Timestamp; if (timeDiff >= m_minFlatline) flatlinedMeasurements.Add(measurement); } } return flatlinedMeasurements; } /// /// 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_flatlineService != null) { m_flatlineService.ServiceProcessException -= m_flatlineService_ServiceProcessException; m_flatlineService.Dispose(); } } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } } // Initialize m_lastChange with dummy measurements so the adapter // can detect measurements that have never been published private void InitializeLastChange() { Ticks timestamp = base.RealTime; foreach (MeasurementKey key in InputMeasurementKeys) { // Try to find the signal ID and add the measurement to the m_lastChange collection if (DataSource.Tables.Contains("ActiveMeasurements")) { DataRow[] measurementRows = DataSource.Tables["ActiveMeasurements"].Select($"ID = '{key}'"); if (measurementRows.Length > 0) { string signalID = measurementRows[0]["SignalID"].ToNonNullString(); if (signalID != "") { m_lastChange.Add(key, new Measurement { //Assumption made that key.SignalID == signalId Metadata = key.Metadata, Value = double.NaN, Timestamp = timestamp }); } } } // If the signal ID could not be found, add the measurement without it if (!m_lastChange.ContainsKey(key)) { m_lastChange.Add(key, new Measurement { Metadata = key.Metadata, Value = double.NaN, Timestamp = timestamp }); } } } // Sends email notifications about changes in the flat-lined status of measurements. private void SendEmailNotifications() { Ticks now = DateTime.UtcNow.Ticks; ICollection allFlatlinedMeasurements = GetFlatlinedMeasurements(); IEnumerable flatlined, noLongerFlatlined; lock (m_lastChange) { flatlined = m_lastNotified .Where(pair => now - pair.Value > m_emailInterval) .Select(pair => m_lastChange[pair.Key]) .Concat(allFlatlinedMeasurements.Where(measurement => !m_lastNotified.Keys.Contains(measurement.Key))) .ToList(); noLongerFlatlined = m_lastNotified .Where(pair => allFlatlinedMeasurements.All(measurement => pair.Key != measurement.Key)) .Select(pair => m_lastChange[pair.Key]) .ToList(); } if (flatlined.Any()) SendEmailNotification(flatlined, true); if (noLongerFlatlined.Any()) SendEmailNotification(noLongerFlatlined, false); } // Send an email address notifying the admin of a changes in the flat-lined status of measurements. private void SendEmailNotification(IEnumerable measurements, bool flatlined) { Ticks now = DateTime.Now.Ticks; Mail message = new Mail("notifications@company.com", m_adminEmailAddress, m_smtpServer); StringBuilder body = new StringBuilder(); body.AppendLine("Measurement Key, Value, Timestamp"); foreach (IMeasurement measurement in measurements) { body.Append(measurement.Key); body.Append(", "); body.Append(measurement.AdjustedValue); body.Append(", "); body.Append(measurement.Timestamp); body.AppendLine(); if (flatlined) m_lastNotified[measurement.Key] = now; else m_lastNotified.Remove(measurement.Key); } if (flatlined) message.Subject = "Flat-lined measurements"; else message.Subject = "No longer flat-lined measurements"; message.Body = body.ToString(); message.Send(); message.Dispose(); } private void m_warningTimer_Elapsed(object sender, ElapsedEventArgs e) { ICollection flatlinedMeasurements = GetFlatlinedMeasurements(); foreach (IMeasurement measurement in flatlinedMeasurements) { Ticks timeDiff = RealTime - measurement.Timestamp; OnStatusMessage(MessageLevel.Info, $"{measurement} flat-lined for {(int)timeDiff.ToSeconds()} seconds."); } } private void m_flatlineService_ServiceProcessException(object sender, EventArgs e) { OnProcessException(MessageLevel.Warning, e.Argument); } #endregion } }