如何为 PI AF 创建自定义数据引用以访问历史数据






4.96/5 (15投票s)
从 AF 表获取指定时间范围内历史值的历史值
引言
撰写本文的原因是网络上关于自定义数据引用的信息稀缺,并且对 PI 资源的访问有限,这样其他试图实现类似功能的人就不必经历那些绞尽脑汁的会议了。尽管 VCampus 上有很多资料,但我仅仅是为了通过将所有信息整合到一个地方来简化事情。
我们已经有了实时管理系统,为什么还需要连接到关系数据库呢?
让我们考虑以下石油和天然气行业的场景。假设有十口油井,我们需要检查出口流量。公司无法为每口油井都安装一个流量计,因为那会花费很多,所以他们决定在十口油井之间安装一个流量计,油井通过管道连接到流量计,每根管道上都有一个阀门。现在假设我们需要检查 1 号油井的出口流量,我们关闭所有其他油井的阀门,由于 1 号油井已经连接到流量计,我们可以轻松获得出口流量值。
从上面的场景中我们可以推断出两件事:如果每口油井都安装一个流量计会增加成本,而且当我们甚至不想每天监测这些数据时,将其存档到 PI 中是毫无意义的。假设有 100 口油井,我们每两周只检查一次 1 号油井的出口流量,那么将这些数据保存在 PI 中是毫无意义的,因为它将完全违背其实时数据管理的主要目的。在这种情况下,将数据保存在某些关系数据库中是明智的选择。
这里 возникает的问题是,如果数据是分散的,即一部分在 PI 系统中,一部分在关系数据库中,那么 PI 是如何管理数据的?PI 提供了显示来自 PI 服务器以及关系数据库的数据的功能。它以表格的形式显示关系数据库中的数据,这些表格可以根据用户的需求进行定制。表格可以在 PI 系统的“库”部分找到。
(图 1.1)
现在下一个大问题是,PI 已经提供了引用关系数据库的方法,为什么我们还需要自定义数据引用?
通常,PI AF 从在 AF 中引用为 PI Point 的 PI 标签获取历史数据,数据也可以从其他数据源(如 Oracle、SQL 等关系数据)中检索,这些数据通过库表引入 AF。AF 不提供从关系数据库中检索特定时间范围内的历史数据的选项,它只在通过 AF TableLookup 数据引用时提供属性的当前值。但是,如果我们需要特定日期时间范围内的历史数据用于应用程序中的分析、趋势目的,该怎么办?因此,我们将在 AF 中创建此 My_Table_Lookup 自定义数据引用,以从关系数据库(例如 SQL)中获取特定时间范围内的历史数据。
基础知识
我假设阅读本文的用户熟悉 PI System Explorer,不过在跳到实现部分之前,我们还是快速回顾一下 AF 的术语吧。
PI System: OSIsoft 产品,用于实时数据和事件管理。
PI AF: PI Asset Framework 允许以物理资产(如工厂内的设备、装置)为参考来表示数据。它以元素和属性的形式显示层次化和结构化的数据。
元素: 元素通常表示工厂内的物理对象,如设备、油井等。
属性: 属性用于存储有关元素的信息,如流量、温度等。
数据引用: 数据引用允许从数据源(可以是 PI Point(来自 PI Server)、关系数据库)获取属性值。PI AF 也提供
PI SDK: OSIsoft 提供了 PI 软件开发工具包,一个 Microsoft .NET 程序集,用于访问 OSIsoft 数据,或者简而言之,一个用于访问 PI Server 的编程库。
使用代码
由于我们需要从备用源获取数据,自定义数据引用(**My_Table_Lookup**)将充当一个查找器,类似于表格查找,用于从关系数据库中检索特定时间范围内的数据。My_Table_Lookup 将使用 AF Data Reference 进行配置。它继承自 AF Data Reference 对象并使用可用的标准函数。此自定义数据引用可以使用命令行进行注册,我们稍后会讨论。注册后,它可以在 AF 客户端的数据引用列表中查看(图 1.3)。My_Table_Lookup 将与提供数据库所需数据的 AF 表相关联。关联后,我们将使用与表格查找数据引用相同的对话框,允许用户配置属性值。它向关系数据库发出动态查询并拉取数据。在这里,**GetValues** 函数被覆盖以从 AF 表返回历史值。
您需要 PI SDK(v.1.3.8 或更高版本)(基于 Microsoft 的组件对象模型)和 AF Client 2010 R2 来构建自定义数据引用。安装 PI System 后,PI SDK 会安装所有必要的库和服务,以访问 PI 历史记录中存储的数据。
这可以通过下图更好地理解。
(图 1.2)
基本思想是覆盖数据引用现有的 GetValue 和 GetValues 函数,并添加所需的实现以根据需要获取当前/历史值。
GetValue
默认情况下,此方法返回属性的当前值,由于我们需要特定时间戳的值,我们将覆盖此函数以获取所需结果。
如果请求的时间戳没有可用的值,我们可能需要通过插值结果来返回最接近时间戳的值。
GetValue(object context, object timeContext, AFAttributeList inputAttributes, AFValues inputValues)
首先,我们将覆盖 GetValue 方法
public override AFValue GetValue(object context, object timeContext,
AFAttributeList inputAttributes, AFValues inputValues)
{
.
.
// Code
.
.
}
接下来是获取 Filter,这里 base.Filter
中的 Filter 是我们配置特定属性后得到的查询。
Filter 可能看起来像 "SELECT ProducedOil FROM [WellDailyData] WHERE ProducedOil = @[Production Rate]"。
string filter = base.Filter.Replace("[", "");
filter = filter.Replace("]", "");
我们需要通过解析筛选器从筛选器中检索属性名称
index = filter.IndexOf("@");
if (index > 0)
{
for (int i = index + 1; (i < filter.Length); i++)
{
if ((filter[i].ToString() != " "))
{
attributeName += filter[i].ToString();
}
else
break;
}
........
///code
........
}
创建一个 AFAttribute
对象,因为我们已经检索到了属性名称。我们通过 GetAttribute 方法获取属性对象。
AFAttribute attribute; = base.GetAttribute(attributeName); string val = attribute.GetValue(); string temp = "@" + attributeName; string value = "'" + val.ToString() + "'"; filter = filter.Replace(temp, value); filterInterpolation= filter.Replace(temp, value); // for later reference
现在文章的关键来了。由于我们只能获取当前/最新值,我们需要修改并检索特定 TimeStamp
的值。
因此,我们将调整 Filter(查询),以便获取请求的时间戳的值。
if (filter != null && timeContext != null) { filter = filter.Trim(); filter = filter + " AND START_DATETIME ='" + timeContext.ToString() + "'"; } else if (filter == null && timeContext != null) // This is in case when there is no filter { filter = "START_DATETIME ='" + timeContext.ToString() + "'"; }
既然我们已经得到了带有时间戳的所需过滤器,现在我们需要获取 Lookup 引用的数据库和表,并查询该表
AFDatabase afDB = base.Database; AFTable t = afDB.Tables[base.Table]; DataRow[] foundRows = t.Table.Select(filter);
返回最后一个值,即 foundRows 的最后一个值(因为它按升序排列)
字段: 包含要返回的值的字段。
AFValue item = item = new AFValue(foundRows[foundRows.Length - 1][base.Field],
new AFTime(Convert.ToDateTime(foundRows[foundRows.Length - 1]["START_DATETIME"])));
现在的问题是,如果请求的时间戳在 AF 表中没有匹配值怎么办?在这种情况下,我们需要对结果进行插值。
插值是在已知数据点的范围内创建新的数据点,即我们需要获取请求时间戳之前和之后最近的时间戳的值。
我们将使用最基本的插值方法,通过找到两个值之间的斜率,其中时间戳表示 Y 轴,值表示 X 轴。
插值
首先,按 DateTime
的升序检索所有满足过滤条件的行
retrievedRows = t.Table.Select(filterInterpolation, "START_DATETIME ASC"); // Or in case there is no filter retrievedRows = t.Table.Select();
现在,查找相应时间戳下的值
for (int i = 0; i < retrievedRows.Length; i++) { if ((DateTime)retrievedRows[i]["START_DATETIME"] > ((AFTime)timeContext).UtcTime) { t2 = (DateTime)retrievedRows[i]["START_DATETIME"]; valueAttribute2 = retrievedRows[i][base.Field].ToString(); break; } else if ((DateTime)retrievedRows[i]["START_DATETIME"] < ((AFTime)timeContext).UtcTime) { t1 = (DateTime)retrievedRows[i]["START_DATETIME"]; valueAttribute1 = retrievedRows[i][base.Field].ToString(); } }
查找两点(即 valueAttribute2 和 valueAttribute1 之间)的斜率
if (valueAttribute1 != "" && valueAttribute2 != "")
{
slope = ((Convert.ToDouble(valueAttribute2)) -
(Convert.ToDouble(valueAttribute1))) / ((t2 - t1).TotalHours);
difference = ((AFTime)timeContext - t2).TotalHours;
interpolatedValue = (Convert.ToDouble(valueAttribute2)) + (difference * slope);
item = new AFValue(interpolatedValue, new AFTime ((AFTime)timeContext).UtcTime);
}
当与请求的 TimeStamp 关联的记录是表中的第一条记录或最后一条记录时,我们也应该处理边界条件。在这种情况下,由于它是第一条或最后一条记录,我们无法找到用于计算斜率的两个点。
// for the first record which is not available else if (valueAttribute1 == "" && valueAttribute2 != "") { interpolatedValue = 0; interpolatedValue = (Convert.ToDouble(valueAttribute2)); item = new AFValue(interpolatedValue, new AFTime((AFTime)timeContext).UtcTime); } //For latest record, if there is no value else if (valueAttribute2 == "" && valueAttribute1 != "") { interpolatedValue = 0; interpolatedValue = (Convert.ToDouble(valueAttribute1)); item = new AFValue(interpolatedValue, new AFTime((AFTime)timeContext).UtcTime); }
最后我们返回所需的 AFValue
return item;
GetValues
此方法检索属性的历史值,并返回特定时间范围(即开始时间和结束时间)内的所有值。它还会根据请求的值数量对结果进行插值。
GetValues(object context, AFTimeRange timeContext, int numberOfValues, AFAttributeList inputAttributes, AFValues[] inputValues)
与 GetValue
方法一样,我们将覆盖 GetValues
方法,获取 Filter 并找到属性名称。
如果 numberOfValues
小于 0,那么我们需要对结果进行插值并根据 numberOfValues
返回值,否则我们将返回 TimeRange
内的所有值。
public override AFValues GetValues(object context,
AFTimeRange timeContext, int numberOfValues,
AFAttributeList inputAttributes, AFValues[] inputValues)
{
.
.
// Code
.
.
}
一旦我们获取了属性名称,我们就会找到 startTime
和 endTime
,即我们需要检索值的 TimeRange
。
startTime = timeContext.StartTime.ToString();
endTime = timeContext.EndTime.ToString();
attribute = base.GetAttribute(attributeName);
val = attribute.GetValue();
temp = "@" + attributeName;
value = "'" + val.ToString() + "'";
filter = filter.Replace(temp, value);
tempfilter = filter.Replace(temp, value);
// as filter would modified therefore we need to maintain it in another variable
现在我们将自定义过滤器
if (filter != null)
{
filter = filter.Trim();
filter = filter + " AND START_DATETIME >='" + startTime +
"' AND START_DATETIME <='" + endTime + "'";
}
else
{
filter = "START_DATETIME >='" + startTime + "' AND START_DATETIME <='" + endTime + "'";
}
我们将检查 numberOfValues
是否为负。如果 numberOfValues
小于 0,我们将对结果进行插值。
DataRow[] retrievedRows; retrievedRows = t.Table.Select(filter); if (numberOfValues < 0) { for (int i = 0; i < retrievedRows.Length; i++) { item = new AFValue(retrievedRows[i][base.Field], new AFTime(Convert.ToDateTime(retrievedRows[i]["START_DATETIME"]))); afValues.Insert(i, item); // item is the AFValue } } else { //Interpolate }
插值
首先,我们将对时间戳进行插值,然后对值进行插值。
针对 numberOfValues
初始化一个时间戳数组,并计算 startTime
和 endTime
之间的时间差,以便我们可以在 TimeRange
内创建时间戳。插值时间将是时间差除以请求值的数量。
numberOfValues = numberOfValues * (-1); DateTime[] Timestamps = new DateTime[numberOfValues]; Timestamps[0] = Convert.ToDateTime(startTime); Timestamps[numberOfValues - 1] = Convert.ToDateTime(startTime); timeDifference = (Convert.ToDateTime(startTime) - Convert.ToDateTime(endTime)).TotalSeconds; if (timeDifference < 0) //if negative timeDifference = timeDifference * (-1); interpolatedTime = (timeDifference / (numberOfValues - 1)); //numberOfValues-1 as starttime and endtime are included for (int i = 1; i < (numberOfValues - 1); i++) { Timestamps[i] = Timestamps[i - 1].AddSeconds(interpolatedTime); }
现在,我们将通过首先检查检索到的记录中是否存在插值时间戳对应的值,如果不存在,则对值进行插值。
我们用时间上下文(包含开始时间和结束时间)找到结果。
DataRow[] retrievedRows; retrievedRows = t.Table.Select(filter, "START_DATETIME ASC");
tempFilter
是筛选器,即不带时间上下文(所有记录按 DateTime 升序排列)
我们可以从 retrievedRows 中获取精确值,但我们将使用 foundRows 来查找插值值。
DataRow[] foundRows; foundRows = t.Table.Select(tempfilter, "START_DATETIME ASC");
首先,我们将检查插值时间戳是否与检索到的行的时间戳匹配,如果匹配,则获取第一个精确值,否则需要对值进行插值。
if (Convert.ToDateTime(retrievedRows[_index]["START_DATETIME"]) == Timestamps[j]) { // Code to get the record from the retrieved rows and insert it to the AFValues list }
在时间戳与插值时间戳不匹配的另一种情况下,我们首先比较检索到的记录的时间戳是否小于插值时间戳,如果是,则再次检查它是否小于 foundRow
记录的时间戳,我们获取第一个时间和其值,否则我们获取第二个时间和值进行插值。
if ((DateTime)foundRows[i]["START_DATETIME"] > Convert.ToDateTime(Convert.ToDateTime(Timestamps[j]))) { t2 = (DateTime)foundRows[i]["START_DATETIME"]; valueAttribute2 = foundRows[i][base.Field].ToString(); break; } else if ((DateTime)foundRows[i]["START_DATETIME"] < Convert.ToDateTime(Convert.ToDateTime(Timestamps[j]))) { t1 = (DateTime)foundRows[i]["START_DATETIME"]; valueAttribute1 = foundRows[i][base.Field].ToString(); }
求斜率并得到插值。
slope = ((Convert.ToDouble(valueAttribute2)) - (Convert.ToDouble(valueAttribute1))) / ((t2 - t1).TotalHours); difference = ((AFTime)Timestamps[j] - t2).TotalHours; interpolatedValue = (Convert.ToDouble(valueAttribute2)) + ((difference * slope)); item = new AFValue(interpolatedValue, new AFTime((AFTime)Timestamps[j])); afValues.Insert(j, item);
对所有检索到的记录和插值时间戳重复上述步骤,并返回所有 AFValues。
return afValues;
如何注册和注销自定义数据引用
我们需要注册我们的数据引用才能使用它。
先决条件
将 OSIsoft.AF.Asset.DataReference.TableLookup.dll 和 My_Table_Lookup.dll 放置在以下位置 “C:\Program Files (x86)\PIPC\AF”
用于注册
进入命令提示符并输入 cd C:\Program Files (x86)\PIPC\AF\
输入以下命令
regplugin "C:\Program Files (x86)\PIPC\AF\My_Table_Lookup.dll" /owner:"C:\Program Files (x86)\PIPC\AF\OSIsoft.AF.Asset.DataReference.TableLookup.dll"
如果无法注册数据引用,则输入以下命令强制注册。
regplugin "C:\Program Files (x86)\PIPC\AF\My_Table_Lookup.dll" /owner:"C:\Program Files (x86)\PIPC\AF\OSIsoft.AF.Asset.DataReference.TableLookup.dll" /F
RegPlugIn.exe 位于以下文件夹中:\PIPC\AF
用于注销
进入命令提示符并输入 cd C:\Program Files (x86)\PIPC\AF\
输入以下命令
regplugin /u /Files:C:\ProgramData\OSIsoft\AF\Custom_Table_Lookup.dll
注意:在注册和注销之前,请务必关闭 PI AF 客户端和 PI 服务器(如果已安装)。
注册后,我们可以在 AF 客户端中看到它,如下所示。
(图 1.3)
结论
自定义数据引用可用于各种应用程序,如 Processbook 和 Datalink,用于图形表示、趋势分析等,以从关系数据库中检索历史值。这是一种特定于满足特定需求的数据引用。任何人都可以通过继承 Table Lookup 并根据自己的需求进行自定义来创建数据引用。