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