//******************************************************************************************************
// RangeTest.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/25/2009 - Stephen C. Wills
// Generated original version of source code.
// 12/13/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Timers;
using DataQualityMonitoring.Services;
using GSF;
using GSF.Diagnostics;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
namespace DataQualityMonitoring
{
///
/// Tests measurements to determine whether their values satisfy a range condition.
///
[Description("Range Test: Notifies when the values of received measurements are outside of a given range")]
public class RangeTest : ActionAdapterBase
{
#region [ Members ]
// Constants
///
/// Default low range for frequency measurements.
///
public const double DEFAULT_FREQ_LOW_RANGE = 59.95;
///
/// Default high range for frequency measurements.
///
public const double DEFAULT_FREQ_HIGH_RANGE = 60.05;
///
/// Default low range for voltage phasor magnitudes.
///
public const double DEFAULT_VPHM_LOW_RANGE = 475000.0;
///
/// Default high range for voltage phasor magnitudes.
///
public const double DEFAULT_VPHM_HIGH_RANGE = 525000.0;
///
/// Default low range for current phasor magnitudes.
///
public const double DEFAULT_IPHM_LOW_RANGE = 0.0;
///
/// Default high range for current phasor magnitudes.
///
public const double DEFAULT_IPHM_HIGH_RANGE = 3000.0;
///
/// Default low range for voltage phasor angles.
///
public const double DEFAULT_VPHA_LOW_RANGE = -180.0;
///
/// Default high range for voltage phasor angles.
///
public const double DEFAULT_VPHA_HIGH_RANGE = 180.0;
///
/// Default low range for current phasor angles.
///
public const double DEFAULT_IPHA_LOW_RANGE = -180.0;
///
/// Default high range for current phasor angles.
///
public const double DEFAULT_IPHA_HIGH_RANGE = 180.0;
// Fields
private readonly Dictionary> m_outOfRangeMeasurements;
private string m_signalType;
private double m_lowRange;
private double m_highRange;
private Ticks m_timeToPurge;
private Ticks m_warnInterval;
private readonly Timer m_warningTimer;
private readonly Timer m_purgeTimer;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
public RangeTest()
{
m_outOfRangeMeasurements = new Dictionary>();
m_timeToPurge = Ticks.FromSeconds(1.0);
m_warnInterval = Ticks.FromSeconds(4.0);
m_warningTimer = new Timer();
m_purgeTimer = new Timer();
}
#endregion
#region [ Properties ]
///
/// Gets or sets the signal type of the measurements sent to this range test.
///
[ConnectionStringParameter,
Description("Define the signal type of the measurements sent to this range test. Valid values are FREQ, VPHM, IPHM, VPHA, IPHA. IMPORTANT: If this is not defined, LowRange and HighRange are required parameters."),
DefaultValue(null)]
public string SignalType
{
get
{
return m_signalType;
}
set
{
m_signalType = value;
}
}
///
/// Gets or sets the low range of the measurements sent to this range test.
///
[ConnectionStringParameter,
Description("Define the low range of the measurements sent to this range test."),
DefaultValue("")]
public double LowRange
{
get
{
return m_lowRange;
}
set
{
m_lowRange = value;
}
}
///
/// Gets or sets the high range of the measurements sent to this range test.
///
[ConnectionStringParameter,
Description("Define the high range of the measurements sent to this range test."),
DefaultValue("")]
public double HighRange
{
get
{
return m_highRange;
}
set
{
m_highRange = value;
}
}
///
/// Gets or sets the age, in seconds, at which out-of-range measurements are purged.
///
[ConnectionStringParameter,
Description("Define the age, in seconds, at which out-of-range measurements are purged from the adapter."),
DefaultValue(1.0)]
public double TimeToPurge
{
get
{
return m_timeToPurge.ToSeconds();
}
set
{
m_timeToPurge = Ticks.FromSeconds(value);
}
}
///
/// Gets or sets how often the sends out-of-range notifications to the console.
///
[ConnectionStringParameter,
Description("Define the amount of time, in seconds, between console updates."),
DefaultValue(4.0)]
public double WarnInterval
{
get
{
return m_warnInterval.ToSeconds();
}
set
{
m_warnInterval = Ticks.FromSeconds(value);
}
}
///
/// Gets the flag indicating if this adapter supports temporal processing.
///
public override bool SupportsTemporalProcessing => false;
#endregion
#region [ Methods ]
///
/// Initializes this .
///
public override void Initialize()
{
base.Initialize();
string errorMessage = "{0} is missing from Settings - Example: lowRange=59.95; highRange=60.05";
bool rangeSet = false;
Dictionary settings = Settings;
string setting;
if (settings.TryGetValue("signalType", out m_signalType))
rangeSet = TrySetRange(m_signalType);
if (!rangeSet)
{
// Load required parameters
if (!settings.TryGetValue("lowRange", out setting))
throw new ArgumentException(string.Format(errorMessage, "lowRange"));
m_lowRange = double.Parse(setting);
if (!settings.TryGetValue("highRange", out setting))
throw new ArgumentException(string.Format(errorMessage, "highRange"));
m_highRange = double.Parse(setting);
}
// Load optional parameters
if (settings.TryGetValue("timeToPurge", out setting))
m_timeToPurge = Ticks.FromSeconds(double.Parse(setting));
if (settings.TryGetValue("warnInterval", out setting))
m_warnInterval = Ticks.FromSeconds(double.Parse(setting));
m_warningTimer.Interval = m_warnInterval.ToMilliseconds();
m_warningTimer.Elapsed += m_warningTimer_Elapsed;
m_purgeTimer.Interval = m_timeToPurge.ToMilliseconds();
m_purgeTimer.Elapsed += m_purgeTimer_Elapsed;
}
///
/// Starts the .
///
public override void Start()
{
base.Start();
AttachToService();
m_warningTimer.Start();
}
///
/// Stops the .
///
public override void Stop()
{
base.Stop();
DetachFromService();
m_warningTimer.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.
///
/// If user implemented publication function consistently exceeds available publishing time (i.e., 1 / seconds),
/// concentration will fall behind. A small amount of this time is required by the for processing overhead, so actual total time
/// available for user function process will always be slightly less than 1 / seconds.
///
protected override void PublishFrame(IFrame frame, int index)
{
IMeasurement measurement;
foreach (MeasurementKey key in frame.Measurements.Keys)
{
measurement = frame.Measurements[key];
if (measurement.AdjustedValue < m_lowRange || measurement.AdjustedValue > m_highRange)
AddOutOfRangeMeasurement(key, measurement);
}
}
///
/// Get a dictionary of , pairs representing
/// the number of times each key received has been out-of-range within the last seconds.
///
/// A dictionary of count values for each out-of-range measurement.
public Dictionary GetOutOfRangeCounts()
{
Dictionary outOfRangeCounts = new Dictionary();
lock (m_outOfRangeMeasurements)
{
PurgeOldMeasurements();
int count;
foreach (MeasurementKey key in m_outOfRangeMeasurements.Keys)
{
count = m_outOfRangeMeasurements[key].Count;
if (count > 0)
outOfRangeCounts.Add(key, count);
}
}
return outOfRangeCounts;
}
///
/// Get the full collection of out-of-range s.
///
/// The full collection of out-of-range s.
public ICollection GetOutOfRangeMeasurements()
{
ICollection allOutOfRangeMeasurements = new LinkedList();
lock (m_outOfRangeMeasurements)
{
PurgeOldMeasurements();
foreach (LinkedList measurementList in m_outOfRangeMeasurements.Values)
{
foreach (IMeasurement measurement in measurementList)
allOutOfRangeMeasurements.Add(measurement);
}
}
return allOutOfRangeMeasurements;
}
///
/// Get a collection of out-of-range s with the given key.
///
/// The corresponding to the desired measurements.
/// A collection of out-of-range s.
public ICollection GetOutOfRangeMeasurements(MeasurementKey key)
{
lock (m_outOfRangeMeasurements)
{
PurgeOldMeasurements(key);
return new LinkedList(m_outOfRangeMeasurements[key]);
}
}
// Purge all old out-of-range measurements.
private void PurgeOldMeasurements()
{
lock (m_outOfRangeMeasurements)
{
foreach (MeasurementKey key in m_outOfRangeMeasurements.Keys)
PurgeOldMeasurements(key);
}
}
// Purge old, out-of-range measurements with the given key.
private void PurgeOldMeasurements(MeasurementKey key)
{
lock (m_outOfRangeMeasurements)
{
LinkedList measurements = m_outOfRangeMeasurements[key];
bool donePurging = false;
// Purge old measurements to prevent redundant warnings.
while (measurements.Count > 0 && !donePurging)
{
IMeasurement measurement = measurements.First.Value;
Ticks diff = base.RealTime - measurement.Timestamp;
if (diff >= base.LagTicks + m_timeToPurge)
measurements.RemoveFirst();
else
donePurging = true;
}
}
}
// Try to set the testing range based on the given signal type.
private bool TrySetRange(string signalType)
{
switch (signalType.ToUpper())
{
case "FREQ":
m_lowRange = DEFAULT_FREQ_LOW_RANGE;
m_highRange = DEFAULT_FREQ_HIGH_RANGE;
break;
case "VPHM":
m_lowRange = DEFAULT_VPHM_LOW_RANGE;
m_highRange = DEFAULT_VPHM_HIGH_RANGE;
break;
case "IPHM":
m_lowRange = DEFAULT_IPHM_LOW_RANGE;
m_highRange = DEFAULT_IPHM_HIGH_RANGE;
break;
case "VPHA":
m_lowRange = DEFAULT_VPHA_LOW_RANGE;
m_highRange = DEFAULT_VPHA_HIGH_RANGE;
break;
case "IPHA":
m_lowRange = DEFAULT_IPHA_LOW_RANGE;
m_highRange = DEFAULT_IPHA_HIGH_RANGE;
break;
default:
return false;
}
return true;
}
// Add a measurement to the list of out of range measurements corresponding to the given key.
private void AddOutOfRangeMeasurement(MeasurementKey key, IMeasurement measurement)
{
lock (m_outOfRangeMeasurements)
{
LinkedList outOfRangeList;
if (!m_outOfRangeMeasurements.TryGetValue(key, out outOfRangeList))
{
outOfRangeList = new LinkedList();
m_outOfRangeMeasurements.Add(key, outOfRangeList);
}
outOfRangeList.AddLast(measurement);
}
}
private void AttachToService()
{
lock (s_service)
{
s_service.AttachRangeTest(this);
if (s_exceptionProcessor == null)
s_exceptionProcessor = this;
}
}
private void DetachFromService()
{
lock (s_service)
{
s_service.DetachRangeTest(this);
if (this != s_exceptionProcessor)
return;
ICollection tests = s_service.Tests;
s_exceptionProcessor = tests.Count > 0 ? tests.GetEnumerator().Current : null;
}
}
// Periodically send warnings to the console about out-of-range measurements.
private void m_warningTimer_Elapsed(object sender, ElapsedEventArgs e)
{
Dictionary measurementCounts = GetOutOfRangeCounts();
foreach (MeasurementKey key in measurementCounts.Keys)
{
int count = measurementCounts[key];
OnStatusMessage(MessageLevel.Info, $"Measurement {key} arrived out-of-range {count:N0} times within the last {(int)m_timeToPurge.ToSeconds()} seconds.");
}
}
// Periodically purge old measurements to lighten system load and speed up out-of-range measurement retrieval.
private void m_purgeTimer_Elapsed(object sender, ElapsedEventArgs e)
{
PurgeOldMeasurements();
}
#endregion
#region [ Static ]
// Static Fields
private static readonly OutOfRangeService s_service;
private static RangeTest s_exceptionProcessor;
// Static Constructor
static RangeTest()
{
s_service = new OutOfRangeService();
s_service.ServiceProcessException += s_service_ServiceProcessException;
s_service.Initialize();
}
// Static Methods
private static void s_service_ServiceProcessException(object sender, EventArgs e)
{
s_exceptionProcessor?.OnProcessException(MessageLevel.Warning, e.Argument);
}
#endregion
}
}