//******************************************************************************************************
// 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
}
}