//******************************************************************************************************
// Device.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:
// ----------------------------------------------------------------------------------------------------
// 04/11/2011 - Aniket Salver
// Generated original version of source code.
// 04/21/2011 - Mehulbhai P Thakkar
// Added static methods for database operations.
// 05/02/2011 - J. Ritchie Carroll
// Updated for coding consistency.
// 05/03/2011 - Mehulbhai P Thakkar
// Guid field related changes as well as static functions update.
// 05/05/2011 - Mehulbhai P Thakkar
// Added NULL value and Guid parameter handling for Save() operation.
// 05/12/2011 - Aniket Salver
// Modified the way Guid is retrieved from the Data Base.
// 05/13/2011 - Mehulbhai P Thakkar
// Added regular expression validation for Acronym.
// 05/21/2011 - Mehulbhai P Thakkar
// Modified Save method to create or update associated measurements.
// 09/23/2011 - Mehulbhai P Thakkar
// Added static method to retrieve new devices for output stream.
// 09/14/2012 - Aniket Salver
// Added paging and sorting technique.
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Linq;
using System.Text.RegularExpressions;
using GSF.ComponentModel.DataAnnotations;
using GSF.Configuration;
using GSF.Data;
using GSF.Diagnostics;
using GSF.TimeSeries.UI;
using PhasorProtocolAdapters;
using Measurement = GSF.TimeSeries.UI.DataModels.Measurement;
// ReSharper disable ConstantConditionalAccessQualifier
namespace GSF.PhasorProtocols.UI.DataModels
{
#region [ Enumerations ]
///
/// Device type enumeration.
///
public enum DeviceType
{
///
/// All types of devices
///
///
/// Use this option to return all types of devices.
///
All,
///
/// Concentrator devices such as PDC.
///
///
/// Use this option just to return concentrating devices.
///
Concentrator,
///
/// Direct connected devices such as field PMUs.
///
///
/// Use this option to return directly connected devices.
///
DirectConnected
}
#endregion
///
/// Represents a record of information as defined in the database.
///
public class Device : DataModelBase
{
#region [ Members ]
// Fields
private Guid m_nodeID;
private int? m_parentID;
private Guid m_uniqueID;
private string m_acronym;
private string m_name;
private bool m_isConcentrator;
private int? m_companyID;
private int? m_historianID;
private int m_accessID;
private int? m_vendorDeviceID;
private int? m_protocolID;
private decimal? m_longitude;
private decimal? m_latitude;
private int? m_interconnectionID;
private string m_connectionString;
private string m_timeZone;
private int? m_framesPerSecond;
private long m_timeAdjustmentTicks;
private double m_dataLossInterval;
private bool m_connectOnDemand;
private string m_contactList;
private int? m_measuredLines;
private int m_loadOrder;
private bool m_enabled;
private int m_allowedParsingExceptions;
private double m_parsingExceptionWindow;
private double m_delayedConnectionInterval;
private bool m_allowUseOfCachedConfiguration;
private bool m_autoStartDataParsingSequence;
private bool m_skipDisableRealTimeData;
private int m_measurementReportingInterval;
private string m_companyAcronym;
private string m_alternateCommandChannel;
#endregion
#region [ Properties ]
///
/// Gets or sets NodeID.
///
[Required(ErrorMessage = "Device node ID is a required field, please provide value.")]
public Guid NodeID
{
get => m_nodeID;
set
{
m_nodeID = value;
OnPropertyChanged("NodeID");
}
}
///
/// Gets or sets ID.
///
// Field is populated by database via auto-increment and has no screen interaction, so no validation attributes are applied
public int ID { get; set; }
///
/// Gets or sets ParentID.
///
// Because of database design, no validation attributes are applied.
public int? ParentID
{
get => m_parentID;
set
{
m_parentID = value;
OnPropertyChanged("ParentID");
}
}
///
/// Gets or sets UniqueID.
///
public Guid UniqueID
{
get => m_uniqueID;
set
{
m_uniqueID = value;
OnPropertyChanged("UniqueID");
}
}
///
/// Gets or sets Acronym.
///
[Required(ErrorMessage = "Device acronym is a required field, please provide value.")]
[StringLength(200, ErrorMessage = "Device Acronym cannot exceed 200 characters.")]
[AcronymValidation]
public string Acronym
{
get => m_acronym;
set
{
m_acronym = value.Replace("'", "").Replace(" ", "_").ToUpper();
if (m_acronym.Length > 200)
m_acronym = m_acronym.Substring(0, 200);
OnPropertyChanged("Acronym");
}
}
///
/// Gets or sets Name.
///
[StringLength(200, ErrorMessage = "Device Name cannot exceed 200 characters.")]
public string Name
{
get => m_name;
set
{
m_name = value is null || value.Length <= 200 ? value : value.Substring(0, 200);
OnPropertyChanged("Name");
}
}
///
/// Gets Original Source.
///
public string OriginalSource { get; private set; }
///
/// Gets or sets IsConcenttrator.
///
[DefaultValue(false)]
public bool IsConcentrator
{
get => m_isConcentrator;
set
{
m_isConcentrator = value;
OnPropertyChanged("IsConcentrator");
if (m_isConcentrator)
ParentID = null;
}
}
///
/// Gets or sets CompanyID.
///
// Because of database design, no validation attributes are applied.
public int? CompanyID
{
get => m_companyID;
set
{
m_companyID = value;
OnPropertyChanged("CompanyID");
}
}
///
/// Gets or sets HistrianID.
///
// Because of database design, no validation attributes are applied.
public int? HistorianID
{
get => m_historianID;
set
{
m_historianID = value;
OnPropertyChanged("HistorianID");
}
}
///
/// Gets or sets AccessID.
///
[Required(ErrorMessage = "Device access ID is a required field, please provide value.")]
[DefaultValue(0)]
public int AccessID
{
get => m_accessID;
set
{
m_accessID = value;
OnPropertyChanged("AccessID");
}
}
///
/// Gets or sets VendorDeviceID.
///
// Because of database design, no validation attributes are applied.
public int? VendorDeviceID
{
get => m_vendorDeviceID;
set
{
m_vendorDeviceID = value;
OnPropertyChanged("VendorDeviceID");
}
}
///
/// Gets or sets protocolID.
///
// Because of database design, no validation attributes are applied.
public int? ProtocolID
{
get => m_protocolID;
set
{
m_protocolID = value;
OnPropertyChanged("ProtocolID");
}
}
///
/// Gets or sets Longitude.
///
// Because of database design, no validation attributes are applied.
[RegularExpression(@"^[-]?([0-9]{1,3})?([.][0-9]{1,6})?$", ErrorMessage = "Invalid value. Please provide value in decimal(9,6) format.")]
public decimal? Longitude
{
get => m_longitude;
set
{
m_longitude = value?.Normalize();
OnPropertyChanged("Longitude");
}
}
///
/// Gets or sets Latitude.
///
// Because of database design, no validation attributes are applied.
[RegularExpression(@"^[-]?([0-9]{1,3})?([.][0-9]{1,6})?$", ErrorMessage = "Invalid value. Please provide value in decimal(9,6) format.")]
public decimal? Latitude
{
get => m_latitude;
set
{
m_latitude = value?.Normalize();
OnPropertyChanged("Latitude");
}
}
///
/// Gets or sets InterconnectionID.
///
// Because of database design, no validation attributes are applied.
public int? InterconnectionID
{
get => m_interconnectionID;
set
{
m_interconnectionID = value;
OnPropertyChanged("InterconnectionID");
}
}
///
/// Gets or sets ConnectionString.
///
// Because of database design, no validation attributes are applied.
public string ConnectionString
{
get => m_connectionString;
set
{
m_connectionString = value;
OnPropertyChanged("ConnectionString");
}
}
///
/// Gets or sets TimeZone.
///
[StringLength(200, ErrorMessage = "Device Time Zone cannot exceed 200 characters.")]
public string TimeZone
{
get => m_timeZone;
set
{
m_timeZone = value;
OnPropertyChanged("TimeZone");
}
}
///
/// Gets or sets FramesPerSecond.
///
[DefaultValue(30)]
public int? FramesPerSecond
{
get => m_framesPerSecond;
set
{
m_framesPerSecond = value;
OnPropertyChanged("FramesPerSecond");
}
}
///
/// Gets or sets TimeAdjustmentTicks.
///
[Required(ErrorMessage = "Device time adjustment ticks is a required field, please provide value.")]
[DefaultValue(0L)]
public long TimeAdjustmentTicks
{
get => m_timeAdjustmentTicks;
set
{
m_timeAdjustmentTicks = value;
OnPropertyChanged("TimeAdjustmentTicks");
}
}
///
/// Gets or sets DataLossInterval.
///
[Required(ErrorMessage = "Device data loss interval is a required field, please provide value.")]
[DefaultValue(typeof(double), "5")]
public double DataLossInterval
{
get => m_dataLossInterval;
set
{
m_dataLossInterval = value;
OnPropertyChanged("DataLossInterval");
}
}
///
/// Gets or sets ConnectOnDemand.
///
[Required(ErrorMessage = "ConnectOnDemand is a required field, please provide value.")]
[DefaultValue(false)]
public bool ConnectOnDemand
{
get => m_connectOnDemand;
set
{
m_connectOnDemand = value;
OnPropertyChanged("ConnectOnDemand");
}
}
///
/// Gets or sets ContactList.
///
// Because of database design, no validation attributes are applied.
public string ContactList
{
get => m_contactList;
set
{
m_contactList = value;
OnPropertyChanged("ContactList");
}
}
///
/// Gets or sets MeasuredLines.
///
// Because of database design, no validation attributes are applied.
public int? MeasuredLines
{
get => m_measuredLines;
set
{
m_measuredLines = value;
OnPropertyChanged("MeasuredLines");
}
}
///
/// Gets or sets LoadOrder.
///
[Required(ErrorMessage = "Device load order is a required field, please provide value.")]
[DefaultValue(0)]
public int LoadOrder
{
get => m_loadOrder;
set
{
m_loadOrder = value;
OnPropertyChanged("LoadOrder");
}
}
///
/// Gets or sets Enabled.
///
[DefaultValue(false)]
public bool Enabled
{
get => m_enabled;
set
{
m_enabled = value;
OnPropertyChanged("Enabled");
}
}
///
/// Gets or sets AllowedParsingExceptions.
///
[Required(ErrorMessage = "Device allowed parsing exceptions is a required field, please provide value.")]
[DefaultValue(10)]
public int AllowedParsingExceptions
{
get => m_allowedParsingExceptions;
set
{
m_allowedParsingExceptions = value;
OnPropertyChanged("AllowedParsingExceptions");
}
}
///
/// Gets or sets ParsingExceptionWindow.
///
[Required(ErrorMessage = "Device parsing exception window is a required field, please provide value.")]
[DefaultValue(5.0D)]
public double ParsingExceptionWindow
{
get => m_parsingExceptionWindow;
set
{
m_parsingExceptionWindow = value;
OnPropertyChanged("ParsingExceptionWindow");
}
}
///
/// Gets or sets DelayedConnectionInterval.
///
[Required(ErrorMessage = "Device delayed connection interval is a required field, please provide value.")]
[DefaultValue(5.0D)]
public double DelayedConnectionInterval
{
get => m_delayedConnectionInterval;
set
{
m_delayedConnectionInterval = value;
OnPropertyChanged("DelayedConnectionInterval");
}
}
///
/// Gets or sets AllowUseOfCachedConfiguration.
///
[DefaultValue(true)]
public bool AllowUseOfCachedConfiguration
{
get => m_allowUseOfCachedConfiguration;
set
{
m_allowUseOfCachedConfiguration = value;
OnPropertyChanged("AllowUseOfCachedConfiguration");
}
}
///
/// Gets or sets AutoStartDataParsingSequence.
///
[DefaultValue(true)]
public bool AutoStartDataParsingSequence
{
get => m_autoStartDataParsingSequence;
set
{
m_autoStartDataParsingSequence = value;
OnPropertyChanged("AutoStartDataParsingSequence");
}
}
///
/// Gets or sets SkipDisableRealTimeData.
///
[DefaultValue(false)]
public bool SkipDisableRealTimeData
{
get => m_skipDisableRealTimeData;
set
{
m_skipDisableRealTimeData = value;
OnPropertyChanged("SkipDisableRealTimeData");
}
}
///
/// Gets or sets MeasurementReportingInterval.
///
[Required(ErrorMessage = "Device measurement reporting interval is a required field, please provide value.")]
[DefaultValue(100000)]
public int MeasurementReportingInterval
{
get => m_measurementReportingInterval;
set
{
m_measurementReportingInterval = value;
OnPropertyChanged("MeasurementReportingInterval");
}
}
///
/// Gets CompanyName.
///
public string CompanyName { get; private set; }
///
/// Gets CompanyAcronym.
///
public string CompanyAcronym => string.IsNullOrWhiteSpace(m_companyAcronym) ? s_companyAcronym : m_companyAcronym;
///
/// Gets HistorianAcronym.
///
public string HistorianAcronym { get; private set; }
///
/// Gets VendorDeviceName.
///
public string VendorDeviceName { get; private set; }
///
/// Gets VendorAcronym.
///
public string VendorAcronym { get; private set; }
///
/// Gets ProtocolName.
///
public string ProtocolName { get; private set; }
///
/// Gets ProtocolCategory.
///
public string ProtocolCategory { get; private set; }
///
/// Gets InterconnectionName.
///
public string InterconnectionName { get; private set; }
///
/// Gets NodeName.
///
public string NodeName { get; private set; }
///
/// Gets ParentAcronym.
///
public string ParentAcronym { get; private set; }
///
/// Gets or sets CreatedOn.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public DateTime CreatedOn { get; set; }
///
/// Gets or sets CreatedBy.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public string CreatedBy { get; set; }
///
/// Gets or sets UpdatedOn.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public DateTime UpdatedOn { get; set; }
///
/// Gets or sets UpdatedBy.
///
// Field is populated by database via trigger and has no screen interaction, so no validation attributes are applied
public string UpdatedBy { get; set; }
///
/// Gets or sets alternate command channel.
///
// Field is not saved in database so no validation applied.
public string AlternateCommandChannel
{
get => m_alternateCommandChannel;
set
{
m_alternateCommandChannel = value;
OnPropertyChanged("AlternateCommandChannel");
}
}
#endregion
#region [ Static ]
// Static Fields
private static readonly string s_companyAcronym;
// Static Constructor
static Device()
{
try
{
CategorizedSettingsElementCollection systemSettings = ConfigurationFile.Current.Settings["systemSettings"];
s_companyAcronym = systemSettings["CompanyAcronym"]?.Value;
if (string.IsNullOrWhiteSpace(s_companyAcronym))
s_companyAcronym = "GPA";
}
catch (Exception ex)
{
Logger.SwallowException(ex, "Failed to initialize default company acronym");
}
}
// Static Methods
///
/// LoadKeys information as an style list.
///
/// to connection to database.
/// ID of the parent device to filter data.
/// The text to search by.
/// The field to sort by.
/// ASC or DESC for ascending or descending respectively.
/// Collection of .
public static IList LoadKeys(AdoDataConnection database, int parentID = 0, string searchText = "", string sortMember = "", string sortDirection = "")
{
bool createdConnection = false;
try
{
createdConnection = CreateConnection(ref database);
IList deviceList = new List();
DataTable deviceTable;
string query;
string searchParam = null;
string searchQuery = null;
string sortClause = string.Empty;
if (!string.IsNullOrEmpty(searchText))
{
searchParam = $"%{searchText}%";
if (database.IsJetEngine)
{
// Access queries do not support UPPER but are case-insensitive anyway
searchQuery = string.Format("ID LIKE '{0}' OR UniqueID LIKE '{0}' OR Acronym LIKE '{0}' " +
"OR Name LIKE '{0}' OR OriginalSource LIKE '{0}' OR Longitude LIKE '{0}' OR Latitude LIKE '{0}' " +
"OR ConnectionString LIKE '{0}' OR TimeZone LIKE '{0}' OR FramesPerSecond LIKE '{0}' " +
"OR ContactList LIKE '{0}' OR CompanyName LIKE '{0}' OR CompanyAcronym LIKE '{0}' " +
"OR CompanyMapAcronym LIKE '{0}' OR HistorianAcronym LIKE '{0}' OR VendorAcronym LIKE '{0}' " +
"OR VendorDeviceName LIKE '{0}' OR ProtocolName LIKE '{0}' OR InterconnectionName LIKE '{0}'",
searchParam.Replace("'", "''"));
}
else
{
searchQuery = database.ParameterizedQueryString("ID LIKE {0} OR UPPER(UniqueID) LIKE UPPER({0}) OR UPPER(Acronym) LIKE UPPER({0}) " +
"OR UPPER(Name) LIKE UPPER({0}) OR UPPER(OriginalSource) LIKE UPPER({0}) OR Longitude LIKE {0} OR Latitude LIKE {0} " +
"OR UPPER(ConnectionString) LIKE UPPER({0}) OR UPPER(TimeZone) LIKE UPPER({0}) OR FramesPerSecond LIKE {0} " +
"OR UPPER(ContactList) LIKE UPPER({0}) OR UPPER(CompanyName) LIKE UPPER({0}) OR UPPER(CompanyAcronym) LIKE UPPER({0}) " +
"OR UPPER(CompanyMapAcronym) LIKE UPPER({0}) OR UPPER(HistorianAcronym) LIKE UPPER({0}) OR UPPER(VendorAcronym) LIKE UPPER({0}) " +
"OR UPPER(VendorDeviceName) LIKE UPPER({0}) OR UPPER(ProtocolName) LIKE UPPER({0}) OR UPPER(InterconnectionName) LIKE UPPER({0})",
"searchParam");
}
}
if (!string.IsNullOrEmpty(sortMember))
sortClause = $"ORDER BY {sortMember} {sortDirection}";
if (parentID > 0)
{
if (string.IsNullOrEmpty(searchText))
{
query = database.ParameterizedQueryString($"SELECT ID From DeviceDetail WHERE NodeID = {{0}} AND ParentID = {{1}} {sortClause}", "nodeID", "parentID");
deviceTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout, database.CurrentNodeID(), parentID);
}
else if (!database.IsJetEngine)
{
query = database.ParameterizedQueryString($"SELECT ID From DeviceDetail WHERE NodeID = {{0}} AND ParentID = {{1}} AND ({searchText}) {sortClause}", "nodeID", "parentID");
deviceTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout, database.CurrentNodeID(), parentID, searchParam);
}
else
{
query = database.ParameterizedQueryString($"SELECT ID From DeviceDetail WHERE NodeID = {{0}} AND ParentID = {{1}} AND ({searchText}) {sortClause}", "nodeID", "parentID");
deviceTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout, database.CurrentNodeID(), parentID);
}
}
else
{
if (string.IsNullOrEmpty(searchText))
{
query = database.ParameterizedQueryString($"SELECT ID From DeviceDetail WHERE NodeID = {{0}} {sortClause}", "nodeID");
deviceTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout, database.CurrentNodeID());
}
else if (!database.IsJetEngine)
{
query = database.ParameterizedQueryString($"SELECT ID From DeviceDetail WHERE NodeID = {{0}} AND ({searchQuery}) {sortClause}", "nodeID");
deviceTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout, database.CurrentNodeID(), searchParam);
}
else
{
query = database.ParameterizedQueryString($"SELECT ID From DeviceDetail WHERE NodeID = {{0}} AND ({searchQuery}) {sortClause}", "nodeID");
deviceTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout, database.CurrentNodeID());
}
}
foreach (DataRow row in deviceTable.Rows)
deviceList.Add(row.ConvertField("ID"));
return deviceList;
}
finally
{
if (createdConnection)
database?.Dispose();
}
}
///
/// Loads information as an style list.
///
/// to connection to database.
/// Keys of the measurement to be loaded from the database
/// Collection of .
public static ObservableCollection Load(AdoDataConnection database, IList keys)
{
bool createdConnection = false;
try
{
createdConnection = CreateConnection(ref database);
Device[] deviceList = null;
if (keys is not null && keys.Count > 0)
{
string commaSeparatedKeys = keys.Select(key => key.ToString()).Aggregate((str1, str2) => $"{str1},{str2}");
string query = $"SELECT * FROM DeviceDetail WHERE ID IN ({commaSeparatedKeys})";
DataTable deviceTable = database.Connection.RetrieveData(database.AdapterType, query, DefaultTimeout);
deviceList = new Device[deviceTable.Rows.Count];
foreach (DataRow row in deviceTable.Rows)
{
int id = row.ConvertField("ID");
deviceList[keys.IndexOf(id)] = new Device()
{
NodeID = database.Guid(row, "NodeID"),
ID = id,
ParentID = row.ConvertNullableField("ParentID"),
UniqueID = database.Guid(row, "UniqueID"),
Acronym = row.Field("Acronym"),
Name = row.Field("Name"),
IsConcentrator = Convert.ToBoolean(row.Field