//******************************************************************************************************// DynamicCalculator.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:// ----------------------------------------------------------------------------------------------------// 02/06/2012 - 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.Data;using System.Linq;using System.Reflection;using System.Text;using Ciloci.Flee;using GSF;using GSF.Configuration;using GSF.Diagnostics;using GSF.Threading;using GSF.TimeSeries;using GSF.TimeSeries.Adapters; namespace DynamicCalculator{ /// <summary> /// Represents the source of a timestamp. /// </summary> public enum TimestampSource { /// <summary> /// An incoming frame timestamp. /// </summary> Frame, /// <summary> /// Real-time as defined by the concentration engine. /// </summary> RealTime, /// <summary> /// The system's local clock. /// </summary> LocalClock } /// <summary> /// The DynamicCalculator is an action adapter which takes multiple /// input measurements and performs a calculation on those measurements /// to generate its own calculated measurement. /// </summary> [Description("Dynamic Calculator: Performs arithmetic operations on multiple input signals")] public class DynamicCalculator : ActionAdapterBase { #region [ Members ] // Constants /// <summary> /// Defines the default value for <see cref="Imports"/> property. /// </summary> public const string DefaultImports = "AssemblyName=mscorlib, TypeName=System.Math; AssemblyName=mscorlib, TypeName=System.DateTime"; private const string TimeVariable = "TIME"; private const string UtcTimeVariable = "UTCTIME"; private const string LocalTimeVariable = "LOCALTIME"; private const string SystemNameVariable = "SYSTEMNAME"; private static readonly string[] ReservedVariableNames = { TimeVariable, UtcTimeVariable, LocalTimeVariable, SystemNameVariable }; // Fields private string m_expressionText; private string m_variableList; private string m_imports; private bool m_supportsTemporalProcessing; private readonly ImmediateMeasurements m_latestMeasurements; private Ticks m_latestTimestamp; private double m_latestValue; private readonly HashSet<string> m_variableNames; private readonly Dictionary<MeasurementKey, string> m_keyMapping; private readonly SortedDictionary<int, string> m_nonAliasedTokens; private string m_aliasedExpressionText; private readonly ExpressionContext m_expressionContext; private IDynamicExpression m_expression; private readonly DelayedSynchronizedOperation m_timerOperation; #endregion #region [ Constructors ] /// <summary> /// Creates a new instance of the <see cref="DynamicCalculator"/>. /// </summary> public DynamicCalculator() { m_latestMeasurements = new ImmediateMeasurements { RealTimeFunction = () => RealTime }; m_variableNames = new HashSet<string>(); m_keyMapping = new Dictionary<MeasurementKey, string>(); m_nonAliasedTokens = new SortedDictionary<int, string>(); m_expressionContext = new ExpressionContext(); m_timerOperation = new DelayedSynchronizedOperation(ProcessLatestMeasurements, ex => OnProcessException(MessageLevel.Error, ex)); } #endregion #region [ Properties ] /// <summary> /// Gets or sets output measurements that the action adapter will produce, if any. /// </summary> [ConnectionStringParameter] [Description("Defines primary keys of output measurements the action adapter expects; can be one of a filter expression, measurement key, point tag or Guid.")] [CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] public override IMeasurement[] OutputMeasurements { get => base.OutputMeasurements; set => base.OutputMeasurements = value; } /// <summary> /// Gets or sets the textual representation of the expression. /// </summary> [ConnectionStringParameter] [Description("Define the arithmetic expression used to perform calculations.")] public string ExpressionText { get => m_expressionText; set { m_expressionText = value; PerformAliasReplacement(); } } /// <summary> /// Gets or sets the list of variables used in the expression. /// </summary> [ConnectionStringParameter] [Description("Define the unique list of variables used in the expression. Any defined aliased variables must be unique per defined dynamic calculator or e-mail notifier instance. Note that \"TIME\" is reserved, this returns the current timestamp for the calculation in ticks.")] public string VariableList { get => m_variableList; set { if (m_variableList == value) return; // Store the variable list for get operations m_variableList = value; // Empty the collection of variable names m_variableNames.Clear(); m_keyMapping.Clear(); m_nonAliasedTokens.Clear(); // If the value is null, do not attempt to process it if (value is null) return; // Build the collection of variable names with the new value foreach (string token in value.Split(';')) AddVariable(token); // Perform alias replacement on tokens that were not explicitly aliased PerformAliasReplacement(); // Build the key list which will define the input measurements for this adapter string keyList = m_keyMapping.Keys.Select(key => key.ToString()) .Aggregate((runningKeyList, nextKey) => runningKeyList + ";" + nextKey); // Set the input measurements for this adapter InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(DataSource, true, keyList); } } /// <summary> /// Gets or sets the list of types which define /// methods to be imported into the expression parser. /// </summary> [ConnectionStringParameter] [Description("Define the list of types which define methods to be imported into the expression parser.")] [DefaultValue(DefaultImports)] public string Imports { get => m_imports; set { foreach (string typeDef in value.Split(';')) { try { Dictionary<string, string> parsedTypeDef = typeDef.ParseKeyValuePairs(','); string assemblyName = parsedTypeDef["assemblyName"]; string typeName = parsedTypeDef["typeName"]; Assembly assembly = Assembly.Load(new AssemblyName(assemblyName)); Type type = assembly.GetType(typeName); m_expressionContext.Imports.AddType(type); } catch (Exception ex) { string message = $"Unable to load type from assembly: {typeDef}"; OnProcessException(MessageLevel.Error, new ArgumentException(message, ex)); } } m_imports = value; } } /// <summary> /// Gets the flag indicating if this adapter supports temporal processing. /// </summary> [ConnectionStringParameter] [Description("Define the flag indicating if this adapter supports temporal processing.")] [DefaultValue(false)] public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing; /// <summary> /// Gets or sets the flag indicating whether to use the latest /// received values to fill in values missing from the current frame. /// </summary> [ConnectionStringParameter] [Description("Define the flag indicating if this adapter should use latest values for missing measurements.")] [DefaultValue(true)] public bool UseLatestValues { get; set; } /// <summary> /// Gets or sets the flag indicating whether to skip processing of an output with a value of NaN. /// </summary> [ConnectionStringParameter] [Description("Define the flag indicating whether to skip processing of an output with a value of NaN.")] [DefaultValue(false)] public bool SkipNaNOutput { get; set; } /// <summary> /// Gets or sets the value used when no other value can be determined for a variable. /// </summary> [ConnectionStringParameter] [Description("Defines the value used when no other value can be determined for a variable.")] [DefaultValue(double.NaN)] public double SentinelValue { get; set; } /// <summary> /// Gets or sets the interval at which the adapter should calculate values. /// </summary> /// <remarks> /// Set to zero to disable the timer and calculate values upon receipt of input data. /// </remarks> [ConnectionStringParameter] [Description("Define the interval, in seconds, at which the adapter should calculate values. Zero value executes calculations at received data rate.")] [DefaultValue(0)] public double CalculationInterval { get => m_timerOperation.Delay / 1000.0D; set => m_timerOperation.Delay = (int)(value * 1000.0D); } /// <summary> /// Gets or sets the source of the timestamps of the calculated values. /// </summary> [ConnectionStringParameter] [Description("Define the source of the timestamps of the calculated values.")] [DefaultValue(TimestampSource.Frame)] public TimestampSource TimestampSource { get; set; } /// <summary> /// Gets or sets primary keys of input measurements the dynamic calculator expects. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public override MeasurementKey[] InputMeasurementKeys { get => base.InputMeasurementKeys; set { base.InputMeasurementKeys = value; m_latestMeasurements.ClearMeasurementCache(); } } /// <summary> /// Gets flag that determines if the implementation of the <see cref="DynamicCalculator"/> requires an output measurement. /// </summary> protected virtual bool ExpectsOutputMeasurement => true; /// <summary> /// Gets defined variable collection with current values. /// </summary> protected IDictionary<string, object> Variables => m_expressionContext.Variables; /// <summary> /// Returns the detailed status of the data input source. /// </summary> public override string Status { get { StringBuilder status = new StringBuilder(); status.Append(base.Status); status.AppendLine(); status.AppendLine($" Expression Text: {ExpressionText}"); status.AppendLine($" Variable List: {VariableList}"); status.AppendLine($" Use Latest Values: {UseLatestValues}"); status.AppendLine($" Calculation Interval: {(CalculationInterval == 0.0D ? "At received data rate" : $"{CalculationInterval:N3} seconds")}"); status.AppendLine($" Timestamp Source: {TimestampSource}"); status.AppendLine($" Skip NaN Values: {SkipNaNOutput}"); status.AppendLine($" Sentinel Value: {SentinelValue}"); if (ExpectsOutputMeasurement) status.AppendLine($" Last Calculated Value: {m_latestValue}"); List<string> imports = new List<string>(); if (!string.IsNullOrWhiteSpace(Imports)) { foreach (string typeDef in Imports.Split(';')) { Dictionary<string, string> parsedTypeDef = typeDef.ParseKeyValuePairs(','); if (parsedTypeDef.TryGetValue("typeName", out string typeName)) imports.Add(typeName); } } status.AppendLine(); status.AppendLine($"Imported .NET Types ({imports.Count:N0} total):"); foreach (string import in imports) status.AppendLine($" {import.TruncateRight(70)}"); return status.ToString(); } } private new Ticks RealTime { get { switch (TimestampSource) { case TimestampSource.RealTime: return base.RealTime; case TimestampSource.LocalClock: return DateTime.UtcNow; case TimestampSource.Frame: return m_latestTimestamp; default: return m_latestTimestamp; } } } #endregion #region [ Methods ] /// <summary> /// Initializes <see cref="DynamicCalculator"/>. /// </summary> public override void Initialize() { const string ErrorMessage = "{0} is missing from Settings - Example: expressionText=x+y; variableList={{x = PPA:1; y = PPA:2}}"; Dictionary<string, string> settings = Settings; base.Initialize(); if (ExpectsOutputMeasurement && OutputMeasurements?.Length != 1) throw new ArgumentException($"Exactly one output measurement must be defined. Amount defined: {OutputMeasurements?.Length ?? 0}"); // Load required parameters if (!settings.TryGetValue(nameof(VariableList), out string setting)) throw new ArgumentException(string.Format(ErrorMessage, nameof(VariableList))); VariableList = setting; if (!settings.TryGetValue(nameof(ExpressionText), out setting)) throw new ArgumentException(string.Format(ErrorMessage, nameof(ExpressionText))); ExpressionText = setting; // Load optional parameters Imports = settings.TryGetValue(nameof(Imports), out setting) ? setting : DefaultImports; m_supportsTemporalProcessing = settings.TryGetValue(nameof(SupportsTemporalProcessing), out setting) && setting.ParseBoolean(); SkipNaNOutput = settings.TryGetValue(nameof(SkipNaNOutput), out setting) && setting.ParseBoolean(); TimestampSource = settings.TryGetValue(nameof(TimestampSource), out setting) && Enum.TryParse(setting, out TimestampSource timestampSource) ? timestampSource : TimestampSource.Frame; CalculationInterval = settings.TryGetValue(nameof(CalculationInterval), out setting) ? double.Parse(setting) : 0; UseLatestValues = !settings.TryGetValue(nameof(UseLatestValues), out setting) || setting.ParseBoolean(); SentinelValue = settings.TryGetValue(nameof(SentinelValue), out setting) ? double.Parse(setting) : double.NaN; m_latestMeasurements.LagTime = LagTime; m_latestMeasurements.LeadTime = LeadTime; } /// <summary> /// Starts the <see cref="DynamicCalculator"/> or restarts it if it is already running. /// </summary> [AdapterCommand("Starts the action adapter or restarts it if it is already running.", "Administrator", "Editor")] public override void Start() { base.Start(); if (UseLatestValues && m_timerOperation.Delay > 0) m_timerOperation.RunOnceAsync(); } /// <summary> /// Publish <see cref="IFrame"/> of time-aligned collection of <see cref="IMeasurement"/> values that arrived within the /// concentrator's defined <see cref="ConcentratorBase.LagTime"/>. /// </summary> /// <param name="frame"><see cref="IFrame"/> of measurements with the same timestamp that arrived within <see cref="ConcentratorBase.LagTime"/> that are ready for processing.</param> /// <param name="index">Index of <see cref="IFrame"/> within a second ranging from zero to <c><see cref="ConcentratorBase.FramesPerSecond"/> - 1</c>.</param> protected override void PublishFrame(IFrame frame, int index) { m_latestTimestamp = frame.Timestamp; if (UseLatestValues) ProcessMeasurements(frame.Measurements.Values); else Calculate(frame.Measurements); } /// <summary> /// Handler for the values calculated by the <see cref="DynamicCalculator"/>. /// </summary> /// <param name="value">The value calculated by the <see cref="DynamicCalculator"/>.</param> protected virtual void HandleCalculatedValue(object value) { // Evaluate the expression and generate the measurement GenerateCalculatedMeasurement(RealTime, value as IConvertible); } private void ProcessMeasurements(IEnumerable<IMeasurement> measurements) { foreach (IMeasurement measurement in measurements) { m_latestMeasurements.UpdateMeasurementValue(measurement); if (measurement.Timestamp > m_latestTimestamp && measurement.Timestamp.UtcTimeIsValid(LagTime, LeadTime)) m_latestTimestamp = measurement.Timestamp; } if (m_timerOperation.Delay <= 0) ProcessLatestMeasurements(); } private void ProcessLatestMeasurements() { if (!Enabled) return; IDictionary<MeasurementKey, IMeasurement> measurementLookup = m_latestMeasurements .Cast<IMeasurement>() .ToDictionary(measurement => measurement.Key); Calculate(measurementLookup); if (m_timerOperation.Delay > 0) m_timerOperation.RunOnceAsync(); } private void Calculate(IDictionary<MeasurementKey, IMeasurement> measurements) { m_expressionContext.Variables.Clear(); // Set the values of variables in the expression foreach (MeasurementKey key in m_keyMapping.Keys) { string name = m_keyMapping[key]; Lines 1–500 of 668. Load more
//****************************************************************************************************** // DynamicCalculator.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: // ---------------------------------------------------------------------------------------------------- // 02/06/2012 - 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.Data; using System.Linq; using System.Reflection; using System.Text; using Ciloci.Flee; using GSF; using GSF.Configuration; using GSF.Diagnostics; using GSF.Threading; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; namespace DynamicCalculator { /// <summary> /// Represents the source of a timestamp. /// </summary> public enum TimestampSource { /// <summary> /// An incoming frame timestamp. /// </summary> Frame, /// <summary> /// Real-time as defined by the concentration engine. /// </summary> RealTime, /// <summary> /// The system's local clock. /// </summary> LocalClock } /// <summary> /// The DynamicCalculator is an action adapter which takes multiple /// input measurements and performs a calculation on those measurements /// to generate its own calculated measurement. /// </summary> [Description("Dynamic Calculator: Performs arithmetic operations on multiple input signals")] public class DynamicCalculator : ActionAdapterBase { #region [ Members ] // Constants /// <summary> /// Defines the default value for <see cref="Imports"/> property. /// </summary> public const string DefaultImports = "AssemblyName=mscorlib, TypeName=System.Math; AssemblyName=mscorlib, TypeName=System.DateTime"; private const string TimeVariable = "TIME"; private const string UtcTimeVariable = "UTCTIME"; private const string LocalTimeVariable = "LOCALTIME"; private const string SystemNameVariable = "SYSTEMNAME"; private static readonly string[] ReservedVariableNames = { TimeVariable, UtcTimeVariable, LocalTimeVariable, SystemNameVariable }; // Fields private string m_expressionText; private string m_variableList; private string m_imports; private bool m_supportsTemporalProcessing; private readonly ImmediateMeasurements m_latestMeasurements; private Ticks m_latestTimestamp; private double m_latestValue; private readonly HashSet<string> m_variableNames; private readonly Dictionary<MeasurementKey, string> m_keyMapping; private readonly SortedDictionary<int, string> m_nonAliasedTokens; private string m_aliasedExpressionText; private readonly ExpressionContext m_expressionContext; private IDynamicExpression m_expression; private readonly DelayedSynchronizedOperation m_timerOperation; #endregion #region [ Constructors ] /// <summary> /// Creates a new instance of the <see cref="DynamicCalculator"/>. /// </summary> public DynamicCalculator() { m_latestMeasurements = new ImmediateMeasurements { RealTimeFunction = () => RealTime }; m_variableNames = new HashSet<string>(); m_keyMapping = new Dictionary<MeasurementKey, string>(); m_nonAliasedTokens = new SortedDictionary<int, string>(); m_expressionContext = new ExpressionContext(); m_timerOperation = new DelayedSynchronizedOperation(ProcessLatestMeasurements, ex => OnProcessException(MessageLevel.Error, ex)); } #endregion #region [ Properties ] /// <summary> /// Gets or sets output measurements that the action adapter will produce, if any. /// </summary> [ConnectionStringParameter] [Description("Defines primary keys of output measurements the action adapter expects; can be one of a filter expression, measurement key, point tag or Guid.")] [CustomConfigurationEditor("GSF.TimeSeries.UI.WPF.dll", "GSF.TimeSeries.UI.Editors.MeasurementEditor")] public override IMeasurement[] OutputMeasurements { get => base.OutputMeasurements; set => base.OutputMeasurements = value; } /// <summary> /// Gets or sets the textual representation of the expression. /// </summary> [ConnectionStringParameter] [Description("Define the arithmetic expression used to perform calculations.")] public string ExpressionText { get => m_expressionText; set { m_expressionText = value; PerformAliasReplacement(); } } /// <summary> /// Gets or sets the list of variables used in the expression. /// </summary> [ConnectionStringParameter] [Description("Define the unique list of variables used in the expression. Any defined aliased variables must be unique per defined dynamic calculator or e-mail notifier instance. Note that \"TIME\" is reserved, this returns the current timestamp for the calculation in ticks.")] public string VariableList { get => m_variableList; set { if (m_variableList == value) return; // Store the variable list for get operations m_variableList = value; // Empty the collection of variable names m_variableNames.Clear(); m_keyMapping.Clear(); m_nonAliasedTokens.Clear(); // If the value is null, do not attempt to process it if (value is null) return; // Build the collection of variable names with the new value foreach (string token in value.Split(';')) AddVariable(token); // Perform alias replacement on tokens that were not explicitly aliased PerformAliasReplacement(); // Build the key list which will define the input measurements for this adapter string keyList = m_keyMapping.Keys.Select(key => key.ToString()) .Aggregate((runningKeyList, nextKey) => runningKeyList + ";" + nextKey); // Set the input measurements for this adapter InputMeasurementKeys = AdapterBase.ParseInputMeasurementKeys(DataSource, true, keyList); } } /// <summary> /// Gets or sets the list of types which define /// methods to be imported into the expression parser. /// </summary> [ConnectionStringParameter] [Description("Define the list of types which define methods to be imported into the expression parser.")] [DefaultValue(DefaultImports)] public string Imports { get => m_imports; set { foreach (string typeDef in value.Split(';')) { try { Dictionary<string, string> parsedTypeDef = typeDef.ParseKeyValuePairs(','); string assemblyName = parsedTypeDef["assemblyName"]; string typeName = parsedTypeDef["typeName"]; Assembly assembly = Assembly.Load(new AssemblyName(assemblyName)); Type type = assembly.GetType(typeName); m_expressionContext.Imports.AddType(type); } catch (Exception ex) { string message = $"Unable to load type from assembly: {typeDef}"; OnProcessException(MessageLevel.Error, new ArgumentException(message, ex)); } } m_imports = value; } } /// <summary> /// Gets the flag indicating if this adapter supports temporal processing. /// </summary> [ConnectionStringParameter] [Description("Define the flag indicating if this adapter supports temporal processing.")] [DefaultValue(false)] public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing; /// <summary> /// Gets or sets the flag indicating whether to use the latest /// received values to fill in values missing from the current frame. /// </summary> [ConnectionStringParameter] [Description("Define the flag indicating if this adapter should use latest values for missing measurements.")] [DefaultValue(true)] public bool UseLatestValues { get; set; } /// <summary> /// Gets or sets the flag indicating whether to skip processing of an output with a value of NaN. /// </summary> [ConnectionStringParameter] [Description("Define the flag indicating whether to skip processing of an output with a value of NaN.")] [DefaultValue(false)] public bool SkipNaNOutput { get; set; } /// <summary> /// Gets or sets the value used when no other value can be determined for a variable. /// </summary> [ConnectionStringParameter] [Description("Defines the value used when no other value can be determined for a variable.")] [DefaultValue(double.NaN)] public double SentinelValue { get; set; } /// <summary> /// Gets or sets the interval at which the adapter should calculate values. /// </summary> /// <remarks> /// Set to zero to disable the timer and calculate values upon receipt of input data. /// </remarks> [ConnectionStringParameter] [Description("Define the interval, in seconds, at which the adapter should calculate values. Zero value executes calculations at received data rate.")] [DefaultValue(0)] public double CalculationInterval { get => m_timerOperation.Delay / 1000.0D; set => m_timerOperation.Delay = (int)(value * 1000.0D); } /// <summary> /// Gets or sets the source of the timestamps of the calculated values. /// </summary> [ConnectionStringParameter] [Description("Define the source of the timestamps of the calculated values.")] [DefaultValue(TimestampSource.Frame)] public TimestampSource TimestampSource { get; set; } /// <summary> /// Gets or sets primary keys of input measurements the dynamic calculator expects. /// </summary> [EditorBrowsable(EditorBrowsableState.Never)] public override MeasurementKey[] InputMeasurementKeys { get => base.InputMeasurementKeys; set { base.InputMeasurementKeys = value; m_latestMeasurements.ClearMeasurementCache(); } } /// <summary> /// Gets flag that determines if the implementation of the <see cref="DynamicCalculator"/> requires an output measurement. /// </summary> protected virtual bool ExpectsOutputMeasurement => true; /// <summary> /// Gets defined variable collection with current values. /// </summary> protected IDictionary<string, object> Variables => m_expressionContext.Variables; /// <summary> /// Returns the detailed status of the data input source. /// </summary> public override string Status { get { StringBuilder status = new StringBuilder(); status.Append(base.Status); status.AppendLine(); status.AppendLine($" Expression Text: {ExpressionText}"); status.AppendLine($" Variable List: {VariableList}"); status.AppendLine($" Use Latest Values: {UseLatestValues}"); status.AppendLine($" Calculation Interval: {(CalculationInterval == 0.0D ? "At received data rate" : $"{CalculationInterval:N3} seconds")}"); status.AppendLine($" Timestamp Source: {TimestampSource}"); status.AppendLine($" Skip NaN Values: {SkipNaNOutput}"); status.AppendLine($" Sentinel Value: {SentinelValue}"); if (ExpectsOutputMeasurement) status.AppendLine($" Last Calculated Value: {m_latestValue}"); List<string> imports = new List<string>(); if (!string.IsNullOrWhiteSpace(Imports)) { foreach (string typeDef in Imports.Split(';')) { Dictionary<string, string> parsedTypeDef = typeDef.ParseKeyValuePairs(','); if (parsedTypeDef.TryGetValue("typeName", out string typeName)) imports.Add(typeName); } } status.AppendLine(); status.AppendLine($"Imported .NET Types ({imports.Count:N0} total):"); foreach (string import in imports) status.AppendLine($" {import.TruncateRight(70)}"); return status.ToString(); } } private new Ticks RealTime { get { switch (TimestampSource) { case TimestampSource.RealTime: return base.RealTime; case TimestampSource.LocalClock: return DateTime.UtcNow; case TimestampSource.Frame: return m_latestTimestamp; default: return m_latestTimestamp; } } } #endregion #region [ Methods ] /// <summary> /// Initializes <see cref="DynamicCalculator"/>. /// </summary> public override void Initialize() { const string ErrorMessage = "{0} is missing from Settings - Example: expressionText=x+y; variableList={{x = PPA:1; y = PPA:2}}"; Dictionary<string, string> settings = Settings; base.Initialize(); if (ExpectsOutputMeasurement && OutputMeasurements?.Length != 1) throw new ArgumentException($"Exactly one output measurement must be defined. Amount defined: {OutputMeasurements?.Length ?? 0}"); // Load required parameters if (!settings.TryGetValue(nameof(VariableList), out string setting)) throw new ArgumentException(string.Format(ErrorMessage, nameof(VariableList))); VariableList = setting; if (!settings.TryGetValue(nameof(ExpressionText), out setting)) throw new ArgumentException(string.Format(ErrorMessage, nameof(ExpressionText))); ExpressionText = setting; // Load optional parameters Imports = settings.TryGetValue(nameof(Imports), out setting) ? setting : DefaultImports; m_supportsTemporalProcessing = settings.TryGetValue(nameof(SupportsTemporalProcessing), out setting) && setting.ParseBoolean(); SkipNaNOutput = settings.TryGetValue(nameof(SkipNaNOutput), out setting) && setting.ParseBoolean(); TimestampSource = settings.TryGetValue(nameof(TimestampSource), out setting) && Enum.TryParse(setting, out TimestampSource timestampSource) ? timestampSource : TimestampSource.Frame; CalculationInterval = settings.TryGetValue(nameof(CalculationInterval), out setting) ? double.Parse(setting) : 0; UseLatestValues = !settings.TryGetValue(nameof(UseLatestValues), out setting) || setting.ParseBoolean(); SentinelValue = settings.TryGetValue(nameof(SentinelValue), out setting) ? double.Parse(setting) : double.NaN; m_latestMeasurements.LagTime = LagTime; m_latestMeasurements.LeadTime = LeadTime; } /// <summary> /// Starts the <see cref="DynamicCalculator"/> or restarts it if it is already running. /// </summary> [AdapterCommand("Starts the action adapter or restarts it if it is already running.", "Administrator", "Editor")] public override void Start() { base.Start(); if (UseLatestValues && m_timerOperation.Delay > 0) m_timerOperation.RunOnceAsync(); } /// <summary> /// Publish <see cref="IFrame"/> of time-aligned collection of <see cref="IMeasurement"/> values that arrived within the /// concentrator's defined <see cref="ConcentratorBase.LagTime"/>. /// </summary> /// <param name="frame"><see cref="IFrame"/> of measurements with the same timestamp that arrived within <see cref="ConcentratorBase.LagTime"/> that are ready for processing.</param> /// <param name="index">Index of <see cref="IFrame"/> within a second ranging from zero to <c><see cref="ConcentratorBase.FramesPerSecond"/> - 1</c>.</param> protected override void PublishFrame(IFrame frame, int index) { m_latestTimestamp = frame.Timestamp; if (UseLatestValues) ProcessMeasurements(frame.Measurements.Values); else Calculate(frame.Measurements); } /// <summary> /// Handler for the values calculated by the <see cref="DynamicCalculator"/>. /// </summary> /// <param name="value">The value calculated by the <see cref="DynamicCalculator"/>.</param> protected virtual void HandleCalculatedValue(object value) { // Evaluate the expression and generate the measurement GenerateCalculatedMeasurement(RealTime, value as IConvertible); } private void ProcessMeasurements(IEnumerable<IMeasurement> measurements) { foreach (IMeasurement measurement in measurements) { m_latestMeasurements.UpdateMeasurementValue(measurement); if (measurement.Timestamp > m_latestTimestamp && measurement.Timestamp.UtcTimeIsValid(LagTime, LeadTime)) m_latestTimestamp = measurement.Timestamp; } if (m_timerOperation.Delay <= 0) ProcessLatestMeasurements(); } private void ProcessLatestMeasurements() { if (!Enabled) return; IDictionary<MeasurementKey, IMeasurement> measurementLookup = m_latestMeasurements .Cast<IMeasurement>() .ToDictionary(measurement => measurement.Key); Calculate(measurementLookup); if (m_timerOperation.Delay > 0) m_timerOperation.RunOnceAsync(); } private void Calculate(IDictionary<MeasurementKey, IMeasurement> measurements) { m_expressionContext.Variables.Clear(); // Set the values of variables in the expression foreach (MeasurementKey key in m_keyMapping.Keys) { string name = m_keyMapping[key];