//****************************************************************************************************** // HomeUserControl.xaml.cs - Gbtc // // Copyright © 2010, 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/09/2011 - Mehulbhai P Thakkar // Generated original version of source code. // 30/07/2012 - Aniket Salver // Remembers the last graph selection. // 05/05/2021 - J.Ritchie Carroll // Added new alarm/warning stats row. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; using System.Xml; using System.Xml.Linq; using System.Xml.Serialization; using GSF.Communication; using GSF.ComponentModel; using GSF.Configuration; using GSF.Data; using GSF.Data.Model; using GSF.Diagnostics; using GSF.IO; using GSF.Identity; using GSF.PhasorProtocols.UI.DataModels; using GSF.Reflection; using GSF.ServiceProcess; using GSF.TimeSeries; using GSF.TimeSeries.Transport; using GSF.TimeSeries.UI; using Microsoft.Research.DynamicDataDisplay; using Microsoft.Research.DynamicDataDisplay.Charts; using Microsoft.Research.DynamicDataDisplay.DataSources; using MeasurementModel = GSF.TimeSeries.UI.DataModels.Measurement; #pragma warning disable 612,618 // ReSharper disable UnusedMember.Local // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable RedundantNameQualifier namespace GSF.PhasorProtocols.UI.UserControls { /// /// Interaction logic for HomeUserControl.xaml /// public partial class HomeUserControl { #region [ Members ] // Nested Types private class CustomActionAdapter { [PrimaryKey(true)] public int ID { get; set; } public string AdapterName { get; set; } public string AssemblyName { get; set; } public string ConnectionString { get; set; } [UpdateValueExpression("DateTime.UtcNow")] public DateTime UpdatedOn { get; set; } [UpdateValueExpression("UserInfo.CurrentUserID")] public string UpdatedBy { get; set; } } // Constants private const string SystemSettings = CommonFunctions.DefaultSettingsCategory; // Fields private readonly ObservableCollection m_menuDataItems; private WindowsServiceClient m_windowsServiceClient; private DispatcherTimer m_refreshTimer; // Subscription fields private DataSubscriber m_chartSubscription; private DataSubscriber m_statsSubscription; private bool m_chartSubscriptionConnected; private string m_signalID; private int m_processingUnsynchronizedMeasurements; private double m_refreshInterval = 0.25; private int[] m_xAxisDataCollection; // Source data for the binding collection. private EnumerableDataSource m_xAxisBindingCollection; // Values plotted on X-Axis. private ConcurrentQueue m_yAxisDataCollection; // Source data for the binding collection. Format is . private EnumerableDataSource m_yAxisBindingCollection; // Values plotted on Y-Axis. private LineGraph m_lineGraph; private int m_numberOfPointsToPlot = 60; private bool m_eventHandlerRegistered; private MeasurementModel m_selectedMeasurement; private Guid[] m_statSignalIDs; private bool m_serverIsLocal; private bool m_timeReasonabilityPopupIsForLocalTime; private double m_lastLocalToServerTimeDeviation; private double m_lastLocalTimeDeviation; private double m_lastServerTimeDeviation; #endregion #region [ Constructor ] /// /// Creates an instance of . /// public HomeUserControl() { InitializeComponent(); this.Loaded += HomeUserControl_Loaded; this.Unloaded += HomeUserControl_Unloaded; // Load Menu XmlRootAttribute xmlRootAttribute = new XmlRootAttribute("MenuDataItems"); XmlSerializer serializer = new XmlSerializer(typeof(ObservableCollection), xmlRootAttribute); using (XmlReader reader = XmlReader.Create(FilePath.GetAbsolutePath("Menu.xml"))) { m_menuDataItems = (ObservableCollection)serializer.Deserialize(reader); } } #endregion #region [ Methods ] private void HomeUserControl_Unloaded(object sender, RoutedEventArgs e) { try { if (!(m_windowsServiceClient?.Helper is null)) m_windowsServiceClient.Helper.ReceivedServiceResponse -= Helper_ReceivedServiceResponse; if (!(m_refreshTimer is null)) m_refreshTimer.Stop(); UnsubscribeChartData(); DisposeStatsSubscription(); } finally { m_refreshTimer = null; m_chartSubscription = null; m_statsSubscription = null; } } private void HomeUserControl_Loaded(object sender, RoutedEventArgs e) { if (!CommonFunctions.CurrentPrincipal.IsInRole("Administrator")) ButtonRestart.IsEnabled = false; if (!CommonFunctions.CurrentPrincipal.IsInRole("Administrator,Editor")) { ButtonInputWizard.IsEnabled = false; ApplyToServer.IsEnabled = false; ApplyToServer.Foreground = new SolidColorBrush(Colors.Gray); ServerTimeLabel.Cursor = null; } m_windowsServiceClient = CommonFunctions.GetWindowsServiceClient(); if (m_windowsServiceClient is null || m_windowsServiceClient.Helper.RemotingClient.CurrentState != ClientState.Connected) { ButtonRestart.IsEnabled = false; m_statsSubscription?.Stop(); } else { m_windowsServiceClient.Helper.ReceivedServiceResponse += Helper_ReceivedServiceResponse; CommonFunctions.SendCommandToService("Health -actionable"); CommonFunctions.SendCommandToService("Version -actionable"); CommonFunctions.SendCommandToService("Status -actionable"); CommonFunctions.SendCommandToService("Time -actionable"); m_eventHandlerRegistered = true; } m_refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; m_refreshTimer.Tick += RefreshTimer_Tick; m_refreshTimer.Start(); TextBlockInstance.Text = IntPtr.Size == 8 ? "64-bit" : "32-bit"; TextBlockLocalTime.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); // Initialize default brush if (s_defaultBrush is null) s_defaultBrush = TextBlockInstance.Background; // Get needed styles if (s_underlineStyle is null) s_underlineStyle = FindResource("Underline") as Style; if (s_shadowStyle is null) s_shadowStyle = FindResource("Shadow") as Style; if (s_underlineShadowStyle is null) s_underlineShadowStyle = FindResource("UnderlineShadow") as Style; Version appVersion = AssemblyInfo.EntryAssembly.Version; TextBlockManagerVersion.Text = $"{appVersion.Major}.{appVersion.Minor}.{appVersion.Build}.0"; try { using (AdoDataConnection database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory)) { TextBlockDatabaseType.Text = database.DatabaseType.ToString(); try { if (database.IsSqlite || database.IsJetEngine) { // Extract database file name from connection string for file centric databases TextBlockDatabaseName.Text = FilePath.GetFileName(database.Connection.ConnectionString.ParseKeyValuePairs()["Data Source"]); } else if (database.IsOracle) { // Extract user name from connection string for Oracle databases TextBlockDatabaseName.Text = database.Connection.ConnectionString.ParseKeyValuePairs()["User Id"]; } else { TextBlockDatabaseName.Text = database.Connection.Database; } } catch { // Fall back on database name if file anything fails TextBlockDatabaseName.Text = database.Connection.Database; } } } catch { TextBlockDatabaseName.Text = "Not Available"; } try { using (UserInfo info = new UserInfo(CommonFunctions.CurrentUser)) { TextBlockUser.Text = info.Exists ? info.LoginID : CommonFunctions.CurrentUser; } } catch { TextBlockUser.Text = CommonFunctions.CurrentUser; } ((HorizontalAxis)ChartPlotterDynamic.MainHorizontalAxis).LabelProvider.LabelStringFormat = ""; //Remove legend on the right. Panel legendParent = (Panel)ChartPlotterDynamic.Legend.ContentGrid.Parent; if (!(legendParent is null)) legendParent.Children.Remove(ChartPlotterDynamic.Legend.ContentGrid); ChartPlotterDynamic.NewLegendVisible = false; m_xAxisDataCollection = new int[m_numberOfPointsToPlot]; for (int i = 0; i < m_numberOfPointsToPlot; i++) m_xAxisDataCollection[i] = i; m_xAxisBindingCollection = new EnumerableDataSource(m_xAxisDataCollection); m_xAxisBindingCollection.SetXMapping(x => x); LoadComboBoxDeviceAsync(); InitializeStatsSubscription(); InitializeTimeReasonabilitySettings(); } private void LoadComboBoxDeviceAsync() { Thread t = new Thread(() => { Dictionary deviceLookupList = Device.GetLookupList(null); Dispatcher.BeginInvoke(new Action(() => { ComboBoxDevice.ItemsSource = deviceLookupList; if (Application.Current.Resources.Contains("SelectedDevice_Home")) ComboBoxDevice.SelectedIndex = (int)Application.Current.Resources["SelectedDevice_Home"]; else ComboBoxDevice.SelectedIndex = 0; })); }); t.IsBackground = true; t.Start(); } private void LoadComboBoxMeasurementAsync() { int selectedDeviceID = ((KeyValuePair)ComboBoxDevice.SelectedItem).Key; Thread t = new Thread(() => { ObservableCollection measurements = MeasurementModel.Load(null, selectedDeviceID); ObservableCollection itemsSource = new ObservableCollection(measurements.Where(m => m.SignalSuffix == "PA" || m.SignalSuffix == "FQ")); Dispatcher.BeginInvoke(new Action(() => { ComboBoxMeasurement.ItemsSource = itemsSource; if (Application.Current.Resources.Contains("SelectedMeasurement_Home") && (int)Application.Current.Resources["SelectedMeasurement_Home"] != -1) ComboBoxMeasurement.SelectedIndex = (int)Application.Current.Resources["SelectedMeasurement_Home"]; else ComboBoxMeasurement.SelectedIndex = 0; })); }); t.IsBackground = true; t.Start(); } private void RefreshTimer_Tick(object sender, EventArgs e) { if (!(m_windowsServiceClient?.Helper?.RemotingClient is null) && m_windowsServiceClient.Helper.RemotingClient.CurrentState == ClientState.Connected) { try { ButtonRestart.IsEnabled = CommonFunctions.CurrentPrincipal.IsInRole("Administrator"); ButtonInputWizard.IsEnabled = CommonFunctions.CurrentPrincipal.IsInRole("Administrator,Editor"); if (!m_eventHandlerRegistered) { m_windowsServiceClient.Helper.ReceivedServiceResponse += Helper_ReceivedServiceResponse; CommonFunctions.SendCommandToService("Version -actionable"); m_eventHandlerRegistered = true; } CommonFunctions.SendCommandToService("Health -actionable"); CommonFunctions.SendCommandToService("Time -actionable"); if (PopupStatus.IsOpen) CommonFunctions.SendCommandToService("Status -actionable"); if (!(m_statsSubscription is null) && !m_statsSubscription.Enabled) m_statsSubscription?.Start(); } catch (Exception ex) { Logger.SwallowException(ex); } } else { ButtonRestart.IsEnabled = false; } TextBlockLocalTime.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); } private void ButtonQuickLink_Click(object sender, RoutedEventArgs e) { MenuDataItem item = new MenuDataItem(); string stringToMatch = ((Button)sender).Tag.ToString(); if (!string.IsNullOrEmpty(stringToMatch)) { if (stringToMatch == "Restart") { RestartService(); } else { GetMenuDataItem(m_menuDataItems, stringToMatch, ref item); if (!(item.MenuText is null)) item.Command.Execute(null); } } } // Recursively finds menu item to navigate to when a button is clicked on the UI. private void GetMenuDataItem(ObservableCollection dataItems, string stringToMatch, ref MenuDataItem item) { foreach (MenuDataItem dataItem in dataItems) { if (string.Equals(dataItem.UserControlPath, stringToMatch, StringComparison.OrdinalIgnoreCase)) { item = dataItem; break; } if (dataItem.SubMenuItems.Count > 0) GetMenuDataItem(dataItem.SubMenuItems, stringToMatch, ref item); } } private void RestartService() { try { if (MessageBox.Show("Do you want to restart service?", "Restart Service", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) { CommonFunctions.SendCommandToService("Restart"); MessageBox.Show("Successfully sent RESTART command to the service.", "Restart Service", MessageBoxButton.OK, MessageBoxImage.Information); } } catch { MessageBox.Show($"Failed sent RESTART command to the service.{Environment.NewLine}Service is either offline or disconnected.", "Restart Service", MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// Handles ReceivedServiceResponse event. /// /// Source of the event. /// Event arguments. private void Helper_ReceivedServiceResponse(object sender, EventArgs e) { if (ClientHelper.TryParseActionableResponse(e.Argument, out string sourceCommand, out bool _)) { switch (sourceCommand.ToLowerInvariant()) { case "health": this.Dispatcher.BeginInvoke((Action)delegate { string[] lines = e.Argument.Message.TrimEnd().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); TextBlockSystemHealth.Text = string.Join(Environment.NewLine, lines.Take(21)); GroupBoxSystemHealth.Header = $"System Health (Last Refreshed: {DateTime.Now:HH:mm:ss.fff})"; }); break; case "status": this.Dispatcher.BeginInvoke((Action)delegate { string trimEnd(string value) { const int MaxLength = 80; if (string.IsNullOrEmpty(value)) return ""; return value.Length <= MaxLength ? value : string.Concat(value.Substring(0, MaxLength - 3), "..."); } string[] lines = e.Argument.Message.TrimEnd().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); TextBlockStatus.Text = string.Join(Environment.NewLine, lines.Select(trimEnd)); GroupBoxStatus.Header = $"System Status (Last Refreshed: {DateTime.Now:HH:mm:ss.fff})"; }); break; case "version": this.Dispatcher.BeginInvoke((Action)delegate { TextBlockVersion.Text = e.Argument.Message.Substring(e.Argument.Message.ToLower().LastIndexOf("version:", StringComparison.Ordinal) + 8).Trim(); }); break; case "time": this.Dispatcher.BeginInvoke((Action)delegate { string[] lines = e.Argument.Message.TrimEnd().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); if (lines.Length > 0) { string[] currentServiceTimes = lines[0].Split(','); if (currentServiceTimes.Length > 0) { string currentServiceTime = currentServiceTimes[0].Substring(currentServiceTimes[0].ToLower().LastIndexOf("system time:", StringComparison.Ordinal) + 12).Trim(); if (DateTime.TryParse(currentServiceTime, out DateTime serviceTime) && DateTime.TryParse(TextBlockLocalTime.Text, out DateTime localTime)) m_lastLocalToServerTimeDeviation = new Ticks(localTime.Ticks - serviceTime.Ticks).ToSeconds(); TextBlockServerTime.Text = currentServiceTime; } } }); break; } } } private void ButtonStatus_Click(object sender, RoutedEventArgs e) { PopupStatus.IsOpen = true; if (!(m_windowsServiceClient?.Helper?.RemotingClient is null) && m_windowsServiceClient.Helper.RemotingClient.CurrentState == ClientState.Connected) CommonFunctions.SendCommandToService("Status -actionable"); } private void ButtonClose_Click(object sender, RoutedEventArgs e) { PopupStatus.IsOpen = false; } private void ComboBoxMeasurement_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (ComboBoxMeasurement.Items.Count > 0) { m_selectedMeasurement = (MeasurementModel)ComboBoxMeasurement.SelectedItem; // Capture's the Selected index of measurement Combo Box if (Application.Current.Resources.Contains("SelectedMeasurement_Home")) { Application.Current.Resources.Remove("SelectedMeasurement_Home"); Application.Current.Resources.Add("SelectedMeasurement_Home", ComboBoxMeasurement.SelectedIndex); } else Application.Current.Resources.Add("SelectedMeasurement_Home", ComboBoxMeasurement.SelectedIndex); if (!(m_selectedMeasurement is null)) { m_signalID = m_selectedMeasurement.SignalID.ToString(); switch (m_selectedMeasurement.SignalSuffix) { case "PA": ChartPlotterDynamic.Visible = DataRect.Create(0, -180, m_numberOfPointsToPlot, 180); break; case "FQ": { double frequencyMin = Convert.ToDouble(IsolatedStorageManager.ReadFromIsolatedStorage("FrequencyRangeMin")); double frequencyMax = Convert.ToDouble(IsolatedStorageManager.ReadFromIsolatedStorage("FrequencyRangeMax")); ChartPlotterDynamic.Visible = DataRect.Create(0, Math.Min(frequencyMin, frequencyMax), m_numberOfPointsToPlot, Math.Max(frequencyMin, frequencyMax)); break; } } } } else { m_signalID = string.Empty; } SubscribeChartData(); } private void ComboBoxDevice_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Capture's the Selected index of Device Combo Box if (Application.Current.Resources.Contains("SelectedDevice_Home")) { Application.Current.Resources.Remove("SelectedDevice_Home"); Application.Current.Resources.Add("SelectedDevice_Home", ComboBoxDevice.SelectedIndex); } else { Application.Current.Resources.Add("SelectedDevice_Home", ComboBoxDevice.SelectedIndex); } LoadComboBoxMeasurementAsync(); } private void TimeReasonability_PreviewTextInput(object sender, TextCompositionEventArgs e) { Regex regex = new Regex("[^0-9.]+"); e.Handled = regex.IsMatch(e.Text); if (!e.Handled) RecommendedValues.Visibility = Visibility.Collapsed; } // Show time reasonability large value warning when either lag time or lead time value is greater than 10 seconds private void TimeReasonability_TextChanged(object sender, TextChangedEventArgs e) => LargeValueWarning.Visibility = double.TryParse(LagTime.Text, out double lagTime) && lagTime > 10.0D || double.TryParse(LeadTime.Text, out double leadTime) && leadTime > 10.0D ? Visibility.Visible : Visibility.Collapsed; private void LocalTimeLabel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { double timeDeviation = m_lastLocalTimeDeviation; if (double.IsNaN(timeDeviation) || double.IsInfinity(timeDeviation) || timeDeviation == default) timeDeviation = s_timeAlarm; double lagTime = Math.Ceiling(timeDeviation * 1.2D); double leadTime = Math.Ceiling(lagTime * 1.2D); LagTime.Text = lagTime.ToString("N2"); LeadTime.Text = leadTime.ToString("N2"); RecommendedValues.Visibility = Visibility.Visible; TimeReasonabilityGroupBox.Header = "Change Local Time Reasonability Parameters"; m_timeReasonabilityPopupIsForLocalTime = true; ApplyToManager.Visibility = m_serverIsLocal ? Visibility.Visible : Visibility.Collapsed; ApplyToManager.IsChecked = true; ApplyToServer.Visibility = m_serverIsLocal ? Visibility.Visible : Visibility.Collapsed; ApplyToServer.IsChecked = !m_serverIsLocal; LabelManagerUpdateNote.Visibility = Visibility.Visible; LabelServerUpdateNote.Visibility = Visibility.Collapsed; TimeReasonabilityPopup.IsOpen = true; LagTime.Focus(); } private void LocalTimeLabel_MouseEnter(object sender, MouseEventArgs e) => LocalTimeLabel.FontWeight = FontWeights.Bold; private void LocalTimeLabel_OnMouseLeave(object sender, MouseEventArgs e) => LocalTimeLabel.FontWeight = FontWeights.Normal; private void ServerTimeLabel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (!ApplyToServer.IsEnabled) return; double timeDeviation = m_lastServerTimeDeviation; if (double.IsNaN(timeDeviation) || double.IsInfinity(timeDeviation) || timeDeviation == default) timeDeviation = s_timeAlarm; double lagTime = Math.Ceiling(timeDeviation * 1.2D); double leadTime = Math.Ceiling(lagTime * 1.2D); LagTime.Text = lagTime.ToString("N2"); LeadTime.Text = leadTime.ToString("N2"); RecommendedValues.Visibility = Visibility.Visible; TimeReasonabilityGroupBox.Header = "Change Server Time Reasonability Parameters"; m_timeReasonabilityPopupIsForLocalTime = false; ApplyToManager.Visibility = Visibility.Collapsed; ApplyToManager.IsChecked = true; ApplyToServer.Visibility = Visibility.Collapsed; ApplyToServer.IsChecked = true; LabelManagerUpdateNote.Visibility = Visibility.Collapsed; LabelServerUpdateNote.Visibility = Visibility.Visible; TimeReasonabilityPopup.IsOpen = true; LagTime.Focus(); } private void ServerTimeLabel_MouseEnter(object sender, MouseEventArgs e) => ServerTimeLabel.FontWeight = ApplyToServer.IsEnabled ? FontWeights.Bold : FontWeights.Normal; private void ServerTimeLabel_OnMouseLeave(object sender, MouseEventArgs e) => ServerTimeLabel.FontWeight = FontWeights.Normal; private void ApplyToManager_OnChecked(object sender, RoutedEventArgs e) { Dispatcher.BeginInvoke(new Action(() => { LabelManagerUpdateNote.Visibility = m_timeReasonabilityPopupIsForLocalTime ? Visibility.Visible : Visibility.Collapsed; VerifyButtonApplyEnabledState(); })); } private void ApplyToManager_OnUnchecked(object sender, RoutedEventArgs e) { Dispatcher.BeginInvoke(new Action(() => { LabelManagerUpdateNote.Visibility = Visibility.Collapsed; VerifyButtonApplyEnabledState(); })); } private void ApplyToServer_OnChecked(object sender, RoutedEventArgs e) { Dispatcher.BeginInvoke(new Action(() => { LabelServerUpdateNote.Visibility = !m_timeReasonabilityPopupIsForLocalTime || m_serverIsLocal ? Visibility.Visible : Visibility.Collapsed; VerifyButtonApplyEnabledState(); })); } private void ApplyToServer_OnUnchecked(object sender, RoutedEventArgs e) { Dispatcher.BeginInvoke(new Action(() => { LabelServerUpdateNote.Visibility = Visibility.Collapsed; VerifyButtonApplyEnabledState(); })); } private void VerifyButtonApplyEnabledState() => ButtonApply.IsEnabled = ApplyToManager.IsChecked.GetValueOrDefault() || ApplyToServer.IsChecked.GetValueOrDefault(); private void ButtonCancel_OnClick(object sender, RoutedEventArgs e) => TimeReasonabilityPopup.IsOpen = false; private void ButtonApply_OnClick(object sender, RoutedEventArgs e) { if (!double.TryParse(LagTime.Text, out double lagTime) || !double.TryParse(LeadTime.Text, out double leadTime)) { MessageBox.Show($"Failed to parse lag time \"{LagTime.Text}\" and/or lead time \"{LeadTime.Text}\" as a floating point value", "Time Reasonability Parse Failure", MessageBoxButton.OK, MessageBoxImage.Error); return; } if (lagTime <= 0.0D || leadTime <= 0.0D) { MessageBox.Show($"Lag time \"{LagTime.Text}\" and/or lead time \"{LeadTime.Text}\" has a value of less than or equal to zero. Values must be greater than zero, but can be a fractional value less than one.", "Time Reasonability Range Error", MessageBoxButton.OK, MessageBoxImage.Error); return; } try { Mouse.OverrideCursor = Cursors.Wait; TimeReasonabilityPopup.IsOpen = false; string lagTimeText = lagTime.ToString(CultureInfo.InvariantCulture); string leadTimeText = leadTime.ToString(CultureInfo.InvariantCulture); if (m_timeReasonabilityPopupIsForLocalTime && ApplyToManager.IsChecked.GetValueOrDefault()) { try { // Update warning and alarm thresholds in manager configuration file s_timeWarning = lagTime * 0.75D; s_timeAlarm = lagTime; // Load home screen threshold settings ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings["HomeScreenThresholds"]; // Make sure needed settings exist settings.Add("TimeWarning", s_timeWarning, "Local clock time deviation warning threshold, in seconds."); settings.Add("TimeAlarm", s_timeAlarm, "Local clock time deviation alarm threshold, in seconds."); // Update settings settings["TimeWarning"].Update(s_timeWarning); settings["TimeAlarm"].Update(s_timeAlarm); // Save changes config.Save(); } catch (Exception ex) { MessageBox.Show($"Failed to update manager warning and alarm configuration thresholds: {ex.Message}", "Time Reasonability Manager Update Failure", MessageBoxButton.OK, MessageBoxImage.Error); } try { // Update internal lead/lag time settings used by manager screens IsolatedStorageManager.WriteToIsolatedStorage("LagTime", lagTimeText); IsolatedStorageManager.WriteToIsolatedStorage("LeadTime", leadTimeText); } catch (Exception ex) { MessageBox.Show($"Failed to update manager lead/lag times in isolated storage: {ex.Message}", "Time Reasonability Manager Update Failure", MessageBoxButton.OK, MessageBoxImage.Error); } } if (ApplyToServer.IsEnabled && (!m_timeReasonabilityPopupIsForLocalTime || m_serverIsLocal) && ApplyToServer.IsChecked.GetValueOrDefault()) { try { // Define common custom action adapter assemblies for which to not apply lead/lag time updates HashSet excludedAssemblies = new HashSet(new[] { "GSF.TimeSeries.dll", "DataQualityMonitoring.dll", "FileAdapters.dll", "sttp.gsf.dll" }, StringComparer.OrdinalIgnoreCase); // Update lead/lag time values in connection strings for each custom action adapter using (AdoDataConnection database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory)) { TableOperations actionAdapterTable = new TableOperations(database); foreach (CustomActionAdapter actionAdapter in actionAdapterTable.QueryRecords()) { if (excludedAssemblies.Contains(actionAdapter.AssemblyName)) continue; string connectionString = actionAdapter.ConnectionString; if (string.IsNullOrWhiteSpace(connectionString)) continue; Dictionary settings = connectionString.ParseKeyValuePairs(); settings["LagTime"] = lagTimeText; settings["LeadTime"] = leadTimeText; actionAdapter.ConnectionString = settings.JoinKeyValuePairs(); actionAdapterTable.UpdateRecord(actionAdapter); try { if (m_windowsServiceClient?.Helper.RemotingClient.CurrentState == ClientState.Connected) CommonFunctions.SendCommandToService($"Init {actionAdapter.AdapterName}"); } catch (Exception ex) { Logger.SwallowException(ex); } } } } catch (Exception ex) { MessageBox.Show($"Failed to update host service custom action adapters lag/lead times: {ex.Message}", "Time Reasonability Server Update Failure", MessageBoxButton.OK, MessageBoxImage.Error); } try { // Update default lead/lag time config file values for calculations XAttribute getElementValue(XElement[] elements, string name) { XElement element = elements.Elements("add").FirstOrDefault(elem => elem.Attributes("name").Any(nameAttribute => string.Compare(nameAttribute.Value, name, StringComparison.OrdinalIgnoreCase) == 0)); return element?.Attributes("value").FirstOrDefault(); } string configPath = FilePath.GetAbsolutePath(ConfigurationFile.Current.Configuration.FilePath.Replace("Manager", "")); if (File.Exists(configPath)) { if (UserHasWriteAccess()) { XDocument hostConfig = XDocument.Load(configPath); XElement[] systemSettings = hostConfig.Descendants(SystemSettings).ToArray(); getElementValue(systemSettings, "DefaultCalculationLagTime").Value = lagTimeText; getElementValue(systemSettings, "DefaultCalculationLeadTime").Value = leadTimeText; hostConfig.Save(configPath); } else if (MessageBox.Show($"All custom action adapter lag/lead times were successfully updated, however, elevated privileges are required in order to update the service configuration file with new default calculation lag/lead time values.{Environment.NewLine}{Environment.NewLine}Do you want to relaunch the manager with elevated privileges so this task can be completed?", "Time Reasonability Server Update Requires Elevation", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) { if (!(Application.Current?.MainWindow is null)) Application.Current.MainWindow.Visibility = Visibility.Hidden; ProcessStartInfo startInfo = new ProcessStartInfo { FileName = Environment.GetCommandLineArgs()[0], Arguments = $"{string.Join(" ", Environment.GetCommandLineArgs().Skip(1))} -elevated", UseShellExecute = true, Verb = "runas" }; using (Process.Start(startInfo)) { } Environment.Exit(0); } } } catch (Exception ex) { MessageBox.Show($"Failed to update host service configuration default calculation lag/lead times: {ex.Message}", "Time Reasonability Server Update Failure", MessageBoxButton.OK, MessageBoxImage.Error); } } } finally { Mouse.OverrideCursor = null; } } private bool UserHasWriteAccess() { try { // Validate that user has write access to the local folder string tempFile = FilePath.GetAbsolutePath(Guid.NewGuid() + ".tmp"); using (File.Create(tempFile)) { } if (File.Exists(tempFile)) File.Delete(tempFile); return true; } catch (Exception ex) { Logger.SwallowException(ex); } return false; } private void SetTimeReasonabilityLabelFontSizes(double fontSize) { LabelCPULabel.FontSize = LabelCPUValue.FontSize = LabelMemoryLabel.FontSize = LabelMemoryValue.FontSize = LabelDiskLabel.FontSize = LabelDiskValue.FontSize = LabelLocalTimeLabel.FontSize = LabelLocalTimeValue.FontSize = LabelServerTimeLabel.FontSize = LabelServerTimeValue.FontSize = fontSize; } private void SetTimeReasonabilityLabelDefaults() { CPUValue.Text = "...%"; UpdateHighlighting(CPULabel, CPUValue, s_defaultBrush, null, null); MemoryValue.Text = "...%"; UpdateHighlighting(MemoryLabel, MemoryValue, s_defaultBrush, null, null); DiskValue.Text = "...%"; UpdateHighlighting(DiskLabel, DiskValue, s_defaultBrush, null, null); LocalTimeValue.Text = $"... {(m_serverIsLocal ? "seconds" : "secs")}"; UpdateHighlighting(LocalTimeLabel, LocalTimeValue, s_defaultBrush, s_underlineStyle, null); ServerTimeValue.Text = $"... {(m_serverIsLocal ? "seconds" : "secs")}"; UpdateHighlighting(ServerTimeLabel, ServerTimeValue, s_defaultBrush, ApplyToServer.IsEnabled ? s_underlineStyle : null, null); } private void InitializeTimeReasonabilitySettings() { if (m_serverIsLocal) { LabelServerTimeSeparator.Visibility = Visibility.Collapsed; LabelServerTimeLabel.Visibility = Visibility.Collapsed; LabelServerTimeValue.Visibility = Visibility.Collapsed; CPULabel.Text = "System CPU Usage:"; MemoryLabel.Text = "System Memory Usage:"; DiskLabel.Text = "Primary Disk Usage:"; SetTimeReasonabilityLabelFontSizes(13); } else { LabelServerTimeSeparator.Visibility = Visibility.Visible; LabelServerTimeLabel.Visibility = Visibility.Visible; LabelServerTimeValue.Visibility = Visibility.Visible; CPULabel.Text = "Sys CPU Usage:"; MemoryLabel.Text = "Sys Mem Usage:"; DiskLabel.Text = "Prim Disk Usage:"; SetTimeReasonabilityLabelFontSizes(11); } SetTimeReasonabilityLabelDefaults(); } private void UserControl_PreviewKeyDown(object sender, KeyEventArgs e) { // Fetch the real key var key = e.Key == Key.System ? e.SystemKey : e.Key; // Check for Ctrl+Shift+S to toggle server local mode if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) && key == Key.S) { TimeReasonabilityPopup.IsOpen = false; m_serverIsLocal = !m_serverIsLocal; InitializeTimeReasonabilitySettings(); } } #region [ Chart Subscription ] private void ChartSubscriptionConnectionTerminated(object sender, EventArgs e) { m_chartSubscriptionConnected = false; UnsubscribeChartData(); try { ChartPlotterDynamic.Dispatcher.BeginInvoke(new Action(() => ChartPlotterDynamic.Children.Remove(m_lineGraph))); } catch (Exception ex) { Logger.SwallowException(ex); } InitializeChartSubscription(); } private void ChartSubscriptionNewMeasurements(object sender, EventArgs> e) { if (0 != Interlocked.Exchange(ref m_processingUnsynchronizedMeasurements, 1)) return; try { foreach (IMeasurement measurement in e.Argument) { double tempValue = measurement.AdjustedValue; if (!double.IsNaN(tempValue) && !double.IsInfinity(tempValue)) // Process data only if it is not NaN or infinity. { ChartPlotterDynamic.Dispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate { if (m_yAxisDataCollection.Count == 0) { for (int i = 0; i < m_numberOfPointsToPlot; i++) m_yAxisDataCollection.Enqueue(tempValue); m_yAxisBindingCollection = new EnumerableDataSource(m_yAxisDataCollection); m_yAxisBindingCollection.SetYMapping(y => y); m_lineGraph = ChartPlotterDynamic.AddLineGraph(new CompositeDataSource(m_xAxisBindingCollection, m_yAxisBindingCollection), Color.FromArgb(255, 25, 25, 200), 1, string.Empty); } else { if (m_yAxisDataCollection.TryDequeue(out double _)) m_yAxisDataCollection.Enqueue(tempValue); } m_yAxisBindingCollection.RaiseDataChanged(); }); } } } finally { Interlocked.Exchange(ref m_processingUnsynchronizedMeasurements, 0); } } private void ChartSubscriptionConnectionEstablished(object sender, EventArgs e) { m_chartSubscriptionConnected = true; SubscribeChartData(); } private void InitializeChartSubscription() { try { using (AdoDataConnection database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory)) { m_chartSubscription = new DataSubscriber(); m_chartSubscription.ConnectionEstablished += ChartSubscriptionConnectionEstablished; m_chartSubscription.NewMeasurements += ChartSubscriptionNewMeasurements; m_chartSubscription.ConnectionTerminated += ChartSubscriptionConnectionTerminated; m_chartSubscription.ConnectionString = database.DataPublisherConnectionString(); m_chartSubscription.Initialize(); m_chartSubscription.Start(); } } catch (Exception ex) { MessageBox.Show($"Failed to initialize subscription.{Environment.NewLine}{ex.Message}", "Failed to Subscribe", MessageBoxButton.OK); } } private void DisposeChartSubscription() { if (m_chartSubscription is null) return; m_chartSubscription.ConnectionEstablished -= ChartSubscriptionConnectionEstablished; m_chartSubscription.NewMeasurements -= ChartSubscriptionNewMeasurements; m_chartSubscription.ConnectionTerminated -= ChartSubscriptionConnectionTerminated; m_chartSubscription.Stop(); m_chartSubscription.Dispose(); m_chartSubscription = null; } private void SubscribeChartData() { if (m_chartSubscription is null) InitializeChartSubscription(); try { ChartPlotterDynamic.Dispatcher.BeginInvoke(new Action(() => ChartPlotterDynamic.Children.Remove(m_lineGraph))); } catch (Exception ex) { Logger.SwallowException(ex); } m_yAxisDataCollection = new ConcurrentQueue(); if (m_chartSubscriptionConnected && m_chartSubscription != null && !string.IsNullOrEmpty(m_signalID)) m_chartSubscription.UnsynchronizedSubscribe(true, true, m_signalID, null, true, m_refreshInterval); } private void UnsubscribeChartData() { try { if (m_chartSubscription != null) { m_chartSubscription.Unsubscribe(); DisposeChartSubscription(); } } catch { m_chartSubscription = null; } } #endregion #region [ Stats Subscription ] private void InitializeStatsSubscription() { try { if (m_statsSubscription is null) { using (AdoDataConnection database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory)) { string connectionString = database.DataPublisherConnectionString(); Dictionary settings = connectionString.ParseKeyValuePairs(); if (settings.TryGetValue("server", out string server)) { try { string[] parts = server.Split(':'); if (parts.Length > 1) server = parts[0]; m_serverIsLocal = Communication.Transport.IsLocalAddress(server); } catch (Exception ex) { Logger.SwallowException(ex); m_serverIsLocal = false; } } m_statsSubscription = new DataSubscriber(); m_statsSubscription.ConnectionEstablished += StatsSubscriptionConnectionEstablished; m_statsSubscription.NewMeasurements += StatsSubscriptionNewMeasurements; m_statsSubscription.ConnectionTerminated += StatsSubscriptionConnectionTerminated; m_statsSubscription.ConnectionString = connectionString; m_statsSubscription.Initialize(); m_statsSubscription.Start(); } } } catch (Exception ex) { Logger.SwallowException(ex); } } private void StatsSubscriptionConnectionEstablished(object sender, EventArgs e) { const string StatsFilterExpression = "SignalAcronym = 'STAT' AND (" + "SignalReference LIKE '%SYSTEM-ST16' OR " + // [0] System CPU Usage "SignalReference LIKE '%SYSTEM-ST20' OR " + // [1] System Memory Usage "SignalReference LIKE '%SYSTEM-ST24' OR " + // [2] System Time Deviation "SignalReference LIKE '%SYSTEM-ST25')"; // [3] Primary Disk Usage m_statSignalIDs = MeasurementModel.LoadSignalIDs(null, StatsFilterExpression, "SignalReference").ToArray(); m_statsSubscription.UnsynchronizedSubscribe(true, true, string.Join(";", m_statSignalIDs), lagTime: 5.0D); } private void StatsSubscriptionConnectionTerminated(object sender, EventArgs e) => Dispatcher.BeginInvoke(new Action(SetTimeReasonabilityLabelDefaults)); private void StatsSubscriptionNewMeasurements(object sender, EventArgs> e) { // [0] System CPU Usage // [1] System Memory Usage // [2] System Time Deviation // [3] Primary Disk Usage foreach (IMeasurement stat in e.Argument) { Dispatcher.BeginInvoke(new Action(() => { Guid signalID = stat.ID; double value = stat.AdjustedValue; string formattedValue = double.IsNaN(value) ? "..." : $"{value:N2}"; if (signalID == m_statSignalIDs[0]) { CPUValue.Text = $"{formattedValue}%"; if (value > s_cpuAlarm) UpdateHighlighting(CPULabel, CPUValue, s_alarmBrush, s_shadowStyle, s_shadowStyle); else if (value > s_cpuWarning) UpdateHighlighting(CPULabel, CPUValue, s_warningBrush, s_shadowStyle, s_shadowStyle); else UpdateHighlighting(CPULabel, CPUValue, s_defaultBrush, null, null); } else if (signalID == m_statSignalIDs[1]) { MemoryValue.Text = $"{formattedValue}%"; if (value > s_memoryAlarm) UpdateHighlighting(MemoryLabel, MemoryValue, s_alarmBrush, s_shadowStyle, s_shadowStyle); else if (value > s_memoryWarning) UpdateHighlighting(MemoryLabel, MemoryValue, s_warningBrush, s_shadowStyle, s_shadowStyle); else UpdateHighlighting(MemoryLabel, MemoryValue, s_defaultBrush, null, null); } else if (signalID == m_statSignalIDs[2]) { ServerTimeValue.Text = $"{formattedValue} {(m_serverIsLocal ? "seconds" : "secs")}"; m_lastServerTimeDeviation = Math.Abs(value); if (m_lastServerTimeDeviation > s_timeAlarm) UpdateHighlighting(ServerTimeLabel, ServerTimeValue, s_alarmBrush, ApplyToServer.IsEnabled ? s_underlineShadowStyle : s_shadowStyle, s_shadowStyle); else if (m_lastServerTimeDeviation > s_timeWarning) UpdateHighlighting(ServerTimeLabel, ServerTimeValue, s_warningBrush, ApplyToServer.IsEnabled ? s_underlineShadowStyle : s_shadowStyle, s_shadowStyle); else UpdateHighlighting(ServerTimeLabel, ServerTimeValue, s_defaultBrush, ApplyToServer.IsEnabled ? s_underlineStyle : null, null); // Calculate local time deviation value += m_lastLocalToServerTimeDeviation; m_lastLocalTimeDeviation = Math.Abs(value); formattedValue = double.IsNaN(value) ? "..." : $"{value:N2}"; LocalTimeValue.Text = $"{formattedValue} {(m_serverIsLocal ? "seconds" : "secs")}"; if (m_lastLocalTimeDeviation > s_timeAlarm) UpdateHighlighting(LocalTimeLabel, LocalTimeValue, s_alarmBrush, s_underlineShadowStyle, s_shadowStyle); else if (m_lastLocalTimeDeviation > s_timeWarning) UpdateHighlighting(LocalTimeLabel, LocalTimeValue, s_warningBrush, s_underlineShadowStyle, s_shadowStyle); else UpdateHighlighting(LocalTimeLabel, LocalTimeValue, s_defaultBrush, s_underlineStyle, null); } else if (signalID == m_statSignalIDs[3]) { DiskValue.Text = $"{formattedValue}%"; if (value > s_diskAlarm) UpdateHighlighting(DiskLabel, DiskValue, s_alarmBrush, s_shadowStyle, s_shadowStyle); else if (value > s_diskWarning) UpdateHighlighting(DiskLabel, DiskValue, s_warningBrush, s_shadowStyle, s_shadowStyle); else UpdateHighlighting(DiskLabel, DiskValue, s_defaultBrush, null, null); } })); } } private void DisposeStatsSubscription() { if (m_statsSubscription is null) return; m_statsSubscription.Stop(); m_statsSubscription.ConnectionEstablished -= StatsSubscriptionConnectionEstablished; m_statsSubscription.NewMeasurements -= StatsSubscriptionNewMeasurements; m_statsSubscription.ConnectionTerminated -= StatsSubscriptionConnectionTerminated; m_statsSubscription.Dispose(); m_statsSubscription = null; } #endregion #endregion #region [ Static ] private const string DefaultWarningBrush = "#FFCAD43E"; private const string DefaultAlarmBrush = "#FFFF1111"; // Static Fields private static readonly double s_cpuWarning = 75.0D; private static readonly double s_cpuAlarm = 95.0D; private static readonly double s_memoryWarning = 75.0D; private static readonly double s_memoryAlarm = 95.0D; private static readonly double s_diskWarning = 75.0D; private static readonly double s_diskAlarm = 95.0D; private static double s_timeWarning = 3.0D; private static double s_timeAlarm = 5.0D; private static readonly SolidColorBrush s_warningBrush; private static readonly SolidColorBrush s_alarmBrush; private static Brush s_defaultBrush; private static Style s_underlineStyle; private static Style s_shadowStyle; private static Style s_underlineShadowStyle; // Static Constructor static HomeUserControl() { try { // Load home screen threshold settings ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings["HomeScreenThresholds"]; // Make sure needed settings exist settings.Add("CPUWarning", s_cpuWarning, "CPU utilization warning threshold, in percent."); settings.Add("CPUAlarm", s_cpuAlarm, "CPU utilization alarm threshold, in percent."); settings.Add("MemoryWarning", s_memoryWarning, "Memory utilization warning threshold, in percent."); settings.Add("MemoryAlarm", s_memoryAlarm, "Memory utilization alarm threshold, in percent."); settings.Add("DiskWarning", s_diskWarning, "Primary disk utilization warning threshold, in percent."); settings.Add("DiskAlarm", s_diskAlarm, "Primary disk utilization alarm threshold, in percent."); settings.Add("TimeWarning", s_timeWarning, "Local clock time deviation warning threshold, in seconds."); settings.Add("TimeAlarm", s_timeAlarm, "Local clock time deviation alarm threshold, in seconds."); settings.Add("WarningColor", DefaultWarningBrush, "Background color for warning labels, commonly yellow."); settings.Add("AlarmColor", DefaultAlarmBrush, "Background color for alarm labels, commonly red."); // Load configured settings s_cpuWarning = settings["CPUWarning"].ValueAs(s_cpuWarning); s_cpuAlarm = settings["CPUAlarm"].ValueAs(s_cpuAlarm); s_memoryWarning = settings["MemoryWarning"].ValueAs(s_memoryWarning); s_memoryAlarm = settings["MemoryAlarm"].ValueAs(s_memoryAlarm); s_diskWarning = settings["DiskWarning"].ValueAs(s_diskWarning); s_diskAlarm = settings["DiskAlarm"].ValueAs(s_diskAlarm); s_timeWarning = settings["TimeWarning"].ValueAs(s_timeWarning); s_timeAlarm = settings["TimeAlarm"].ValueAs(s_timeAlarm); s_warningBrush = new SolidColorBrush((Color)(ColorConverter.ConvertFromString(settings["WarningColor"].ValueAs(DefaultWarningBrush)) ?? Colors.Yellow)); s_alarmBrush = new SolidColorBrush((Color)(ColorConverter.ConvertFromString(settings["AlarmColor"].ValueAs(DefaultAlarmBrush)) ?? Colors.Red)); // Save changes, if any config.Save(); } catch (Exception ex) { Logger.SwallowException(ex); } } // Static Methods private static void UpdateHighlighting(TextBlock statLabel, TextBlock statValue, Brush color, Style statLabelStyle, Style statValueStyle) { void setParentLabelBackground(TextBlock textBlock) { if (textBlock.Parent is Label label) label.Background = color; } setParentLabelBackground(statLabel); statLabel.Style = statLabelStyle; setParentLabelBackground(statValue); statValue.Style = statValueStyle; } #endregion } }