node-opcua-aggregates 2.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mocharc.yml +7 -0
- package/LICENSE +20 -0
- package/bin/sample_aggregate_server.js +15 -0
- package/dist/aggregates.d.ts +7 -0
- package/dist/aggregates.js +201 -0
- package/dist/aggregates.js.map +1 -0
- package/dist/average.d.ts +3 -0
- package/dist/average.js +61 -0
- package/dist/average.js.map +1 -0
- package/dist/common.d.ts +8 -0
- package/dist/common.js +90 -0
- package/dist/common.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.d.ts +16 -0
- package/dist/interpolate.js +135 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/interval.d.ts +49 -0
- package/dist/interval.js +165 -0
- package/dist/interval.js.map +1 -0
- package/dist/minmax.d.ts +18 -0
- package/dist/minmax.js +149 -0
- package/dist/minmax.js.map +1 -0
- package/dist/read_processed_details.d.ts +6 -0
- package/dist/read_processed_details.js +116 -0
- package/dist/read_processed_details.js.map +1 -0
- package/nyc.config.js +16 -0
- package/package.json +51 -0
- package/source/aggregates.ts +277 -0
- package/source/average.ts +74 -0
- package/source/common.ts +118 -0
- package/source/index.ts +17 -0
- package/source/interpolate.ts +201 -0
- package/source/interval.ts +199 -0
- package/source/minmax.ts +231 -0
- package/source/read_processed_details.ts +139 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-aggregates
|
|
3
|
+
*/
|
|
4
|
+
import { AggregateFunction } from "node-opcua-constants";
|
|
5
|
+
import { makeNodeId } from "node-opcua-nodeid";
|
|
6
|
+
import * as utils from "node-opcua-utils";
|
|
7
|
+
import { DataType } from "node-opcua-variant";
|
|
8
|
+
|
|
9
|
+
import { AddressSpace, BaseNode, UAHistoryServerCapabilities, UAHistoryServerCapabilities_Base, UAObject, UAServerCapabilities, UAVariable } from "node-opcua-address-space";
|
|
10
|
+
import { AggregateConfigurationOptionsEx } from "./interval";
|
|
11
|
+
import { AddressSpacePrivate } from "node-opcua-address-space/src/address_space_private";
|
|
12
|
+
import { readProcessedDetails } from "./read_processed_details";
|
|
13
|
+
|
|
14
|
+
// import { HistoryServerCapabilities } from "node-opcua-server";
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
HasProperty Variable AccessHistoryDataCapability Boolean PropertyType Mandatory
|
|
18
|
+
HasProperty Variable AccessHistoryEventsCapability Boolean PropertyType Mandatory
|
|
19
|
+
HasProperty Variable MaxReturnDataValues UInt32 PropertyType Mandatory
|
|
20
|
+
HasProperty Variable MaxReturnEventValues UInt32 PropertyType Mandatory
|
|
21
|
+
HasProperty Variable InsertDataCapability Boolean PropertyType Mandatory
|
|
22
|
+
HasProperty Variable ReplaceDataCapability Boolean PropertyType Mandatory
|
|
23
|
+
HasProperty Variable UpdateDataCapability Boolean PropertyType Mandatory
|
|
24
|
+
HasProperty Variable DeleteRawCapability Boolean PropertyType Mandatory
|
|
25
|
+
HasProperty Variable DeleteAtTimeCapability Boolean PropertyType Mandatory
|
|
26
|
+
HasProperty Variable InsertEventCapability Boolean PropertyType Mandatory
|
|
27
|
+
HasProperty Variable ReplaceEventCapability Boolean PropertyType Mandatory
|
|
28
|
+
HasProperty Variable UpdateEventCapability Boolean PropertyType Mandatory
|
|
29
|
+
HasProperty Variable DeleteEventCapability Boolean PropertyType Mandatory
|
|
30
|
+
HasProperty Variable InsertAnnotationsCapability Boolean PropertyType Mandatory
|
|
31
|
+
*/
|
|
32
|
+
const historicalCapabilitiesDefaultProperties /*: HistoryServerCapabilities */ = {
|
|
33
|
+
accessHistoryDataCapability: true, // Boolean PropertyType Mandatory
|
|
34
|
+
accessHistoryEventsCapability: true, // Boolean PropertyType Mandatory
|
|
35
|
+
deleteAtTimeCapability: false, // Boolean PropertyType Mandatory
|
|
36
|
+
deleteEventCapability: false, // Boolean PropertyType Mandatory
|
|
37
|
+
deleteRawCapability: false, // Boolean PropertyType Mandatory
|
|
38
|
+
insertAnnotationCapability: false, // Boolean PropertyType Mandatory
|
|
39
|
+
insertDataCapability: false, // Boolean PropertyType Mandatory
|
|
40
|
+
insertEventCapability: false, // Boolean PropertyType Mandatory
|
|
41
|
+
maxReturnDataValues: 0,
|
|
42
|
+
maxReturnEventValues: 0, // UInt32 PropertyType Mandatory
|
|
43
|
+
replaceDataCapability: false, // Boolean PropertyType Mandatory
|
|
44
|
+
replaceEventCapability: false, // Boolean PropertyType Mandatory
|
|
45
|
+
updateDataCapability: false, // Boolean PropertyType Mandatory
|
|
46
|
+
updateEventCapability: false // Boolean PropertyType Mandatory
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function createHistoryServerCapabilities(addressSpace: AddressSpace, serverCapabilities: UAServerCapabilities): UAObject {
|
|
50
|
+
/* istanbul ignore next */
|
|
51
|
+
if (serverCapabilities.browseName.toString() !== "ServerCapabilities") {
|
|
52
|
+
throw new Error("Expecting server Capabilities");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const historyServerCapabilitiesType = addressSpace.getNamespace(0).findObjectType("HistoryServerCapabilitiesType")!;
|
|
56
|
+
|
|
57
|
+
/* istanbul ignore next */
|
|
58
|
+
if (!historyServerCapabilitiesType) {
|
|
59
|
+
throw new Error("Cannot find HistoryServerCapabilitiesType");
|
|
60
|
+
}
|
|
61
|
+
return historyServerCapabilitiesType.instantiate({
|
|
62
|
+
browseName: "HistoryServerCapabilities",
|
|
63
|
+
componentOf: serverCapabilities
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function setHistoricalServerCapabilities(historyServerCapabilities: any, defaultProperties: any) {
|
|
68
|
+
function setBoolean(propName: string) {
|
|
69
|
+
const lowerCase = utils.lowerFirstLetter(propName);
|
|
70
|
+
|
|
71
|
+
/* istanbul ignore next */
|
|
72
|
+
if (!defaultProperties.hasOwnProperty(lowerCase)) {
|
|
73
|
+
throw new Error("cannot find " + lowerCase);
|
|
74
|
+
}
|
|
75
|
+
const value = defaultProperties[lowerCase];
|
|
76
|
+
const prop = historyServerCapabilities.getChildByName(propName);
|
|
77
|
+
|
|
78
|
+
/* istanbul ignore next */
|
|
79
|
+
if (!prop) {
|
|
80
|
+
throw new Error(" Cannot find property " + propName);
|
|
81
|
+
}
|
|
82
|
+
prop.setValueFromSource({ dataType: DataType.Boolean, value });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setUInt32(propName: string) {
|
|
86
|
+
const lowerCase = utils.lowerFirstLetter(propName);
|
|
87
|
+
/* istanbul ignore next */
|
|
88
|
+
if (!historyServerCapabilities.hasOwnProperty(lowerCase)) {
|
|
89
|
+
throw new Error("cannot find " + lowerCase);
|
|
90
|
+
}
|
|
91
|
+
const value = defaultProperties[lowerCase];
|
|
92
|
+
const prop = historyServerCapabilities.getChildByName(propName);
|
|
93
|
+
prop.setValueFromSource({ dataType: DataType.UInt32, value });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setBoolean("AccessHistoryDataCapability");
|
|
97
|
+
setBoolean("AccessHistoryEventsCapability");
|
|
98
|
+
|
|
99
|
+
setUInt32("MaxReturnDataValues");
|
|
100
|
+
setUInt32("MaxReturnEventValues");
|
|
101
|
+
|
|
102
|
+
setBoolean("InsertDataCapability");
|
|
103
|
+
setBoolean("ReplaceDataCapability");
|
|
104
|
+
setBoolean("UpdateDataCapability");
|
|
105
|
+
setBoolean("DeleteRawCapability");
|
|
106
|
+
setBoolean("DeleteAtTimeCapability");
|
|
107
|
+
setBoolean("InsertEventCapability");
|
|
108
|
+
setBoolean("ReplaceEventCapability");
|
|
109
|
+
setBoolean("UpdateEventCapability");
|
|
110
|
+
setBoolean("DeleteEventCapability");
|
|
111
|
+
|
|
112
|
+
/// FOUND A BUG HERE spec says InsertAnnotationsCapability
|
|
113
|
+
/// Standard nodeset2 says InsertAnnotationCapability ( without s )
|
|
114
|
+
// xx setBoolean("InsertAnnotationsCapability");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type AggregateFunctionName =
|
|
118
|
+
| "AnnotationCount"
|
|
119
|
+
| "Average"
|
|
120
|
+
| "Count"
|
|
121
|
+
| "Delta"
|
|
122
|
+
| "DeltaBounds"
|
|
123
|
+
| "DurationBad"
|
|
124
|
+
| "DurationGood"
|
|
125
|
+
| "DurationInStateNonZero"
|
|
126
|
+
| "DurationInStateZero"
|
|
127
|
+
| "EndBound"
|
|
128
|
+
| "Interpolative"
|
|
129
|
+
| "Maximum"
|
|
130
|
+
| "Maximum2"
|
|
131
|
+
| "MaximumActualTime"
|
|
132
|
+
| "MaximumActualTime2"
|
|
133
|
+
| "Minimum"
|
|
134
|
+
| "Minimum2"
|
|
135
|
+
| "MinimumActualTime"
|
|
136
|
+
| "MinimumActualTime2"
|
|
137
|
+
| "NumberOfTransitions"
|
|
138
|
+
| "PercentBad"
|
|
139
|
+
| "PercentGood"
|
|
140
|
+
| "Range"
|
|
141
|
+
| "Range2"
|
|
142
|
+
| "StandardDeviationPopulation"
|
|
143
|
+
| "StandardDeviationSample"
|
|
144
|
+
| "Start"
|
|
145
|
+
| "StartBound"
|
|
146
|
+
| "TimeAverage"
|
|
147
|
+
| "TimeAverage2"
|
|
148
|
+
| "Total"
|
|
149
|
+
| "Total2"
|
|
150
|
+
| "VariancePopulation"
|
|
151
|
+
| "VarianceSample"
|
|
152
|
+
| "WorstQuality"
|
|
153
|
+
| "WorstQuality2";
|
|
154
|
+
|
|
155
|
+
interface UAHistoryServerCapabilitiesWithH extends UAServerCapabilities {
|
|
156
|
+
historyServerCapabilities: UAHistoryServerCapabilities;
|
|
157
|
+
}
|
|
158
|
+
function addAggregateFunctionSupport(addressSpace: AddressSpace, functionName: number): void {
|
|
159
|
+
/* istanbul ignore next */
|
|
160
|
+
if (!functionName) {
|
|
161
|
+
throw new Error("Invalid function name");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const serverCapabilities = addressSpace.rootFolder.objects.server.serverCapabilities as UAHistoryServerCapabilitiesWithH;
|
|
165
|
+
|
|
166
|
+
/* istanbul ignore next */
|
|
167
|
+
if (!serverCapabilities.historyServerCapabilities) {
|
|
168
|
+
throw new Error("missing serverCapabilities.historyServerCapabilities");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const aggregateFunctions = serverCapabilities.aggregateFunctions;
|
|
172
|
+
|
|
173
|
+
const aggregateFunctionsInHist = serverCapabilities.historyServerCapabilities.aggregateFunctions;
|
|
174
|
+
|
|
175
|
+
const functionNodeId = makeNodeId(functionName);
|
|
176
|
+
const functionNode = addressSpace.getNamespace(0).findNode(functionNodeId);
|
|
177
|
+
|
|
178
|
+
/* istanbul ignore next */
|
|
179
|
+
if (!functionNode) {
|
|
180
|
+
throw new Error("Cannot find node " + functionName + " in addressSpace");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
aggregateFunctions.addReference({
|
|
184
|
+
nodeId: functionNode.nodeId,
|
|
185
|
+
referenceType: "Organizes"
|
|
186
|
+
});
|
|
187
|
+
aggregateFunctionsInHist.addReference({
|
|
188
|
+
nodeId: functionNode.nodeId,
|
|
189
|
+
referenceType: "Organizes"
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function addAggregateSupport(addressSpace: AddressSpace) {
|
|
194
|
+
const aggregateConfigurationType = addressSpace.getNamespace(0).findObjectType("AggregateConfigurationType");
|
|
195
|
+
|
|
196
|
+
/* istanbul ignore next */
|
|
197
|
+
if (!aggregateConfigurationType) {
|
|
198
|
+
throw new Error("addressSpace do not expose AggregateConfigurationType");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const aggregateFunctionType = addressSpace.getNamespace(0).findObjectType("AggregateFunctionType");
|
|
202
|
+
|
|
203
|
+
/* istanbul ignore next */
|
|
204
|
+
if (!aggregateFunctionType) {
|
|
205
|
+
throw new Error("addressSpace do not expose AggregateFunctionType");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const serverObject = addressSpace.rootFolder.objects.getFolderElementByName("Server");
|
|
209
|
+
|
|
210
|
+
/* istanbul ignore next */
|
|
211
|
+
if (!serverObject) {
|
|
212
|
+
throw new Error("addressSpace do not expose a ServerObject");
|
|
213
|
+
}
|
|
214
|
+
// xx serverObject.
|
|
215
|
+
|
|
216
|
+
const serverCapabilities = serverObject.getChildByName("ServerCapabilities")! as UAServerCapabilities;
|
|
217
|
+
|
|
218
|
+
// Let see if HistoryServer Capabilities object exists
|
|
219
|
+
let historyServerCapabilities = serverCapabilities.getChildByName("HistoryServerCapabilities");
|
|
220
|
+
|
|
221
|
+
/* istanbul ignore next */
|
|
222
|
+
if (!historyServerCapabilities) {
|
|
223
|
+
historyServerCapabilities = createHistoryServerCapabilities(addressSpace, serverCapabilities);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setHistoricalServerCapabilities(historyServerCapabilities, historicalCapabilitiesDefaultProperties);
|
|
227
|
+
|
|
228
|
+
addAggregateFunctionSupport(addressSpace, AggregateFunction.Interpolative);
|
|
229
|
+
addAggregateFunctionSupport(addressSpace, AggregateFunction.Minimum);
|
|
230
|
+
addAggregateFunctionSupport(addressSpace, AggregateFunction.Maximum);
|
|
231
|
+
addAggregateFunctionSupport(addressSpace, AggregateFunction.Average);
|
|
232
|
+
|
|
233
|
+
const addressSpaceInternal = (addressSpace as unknown) as AddressSpacePrivate;
|
|
234
|
+
addressSpaceInternal._readProcessedDetails = readProcessedDetails;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function installAggregateConfigurationOptions(node: UAVariable, options: AggregateConfigurationOptionsEx) {
|
|
238
|
+
const nodePriv = node as any;
|
|
239
|
+
const aggregateConfiguration = nodePriv.$historicalDataConfiguration.aggregateConfiguration;
|
|
240
|
+
aggregateConfiguration.percentDataBad.setValueFromSource({ dataType: "Byte", value: options.percentDataBad });
|
|
241
|
+
aggregateConfiguration.percentDataGood.setValueFromSource({ dataType: "Byte", value: options.percentDataGood });
|
|
242
|
+
aggregateConfiguration.treatUncertainAsBad.setValueFromSource({
|
|
243
|
+
dataType: "Boolean",
|
|
244
|
+
value: options.treatUncertainAsBad
|
|
245
|
+
});
|
|
246
|
+
aggregateConfiguration.useSlopedExtrapolation.setValueFromSource({
|
|
247
|
+
dataType: "Boolean",
|
|
248
|
+
value: options.useSlopedExtrapolation
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
nodePriv.$historicalDataConfiguration.stepped.setValueFromSource({
|
|
252
|
+
dataType: "Boolean",
|
|
253
|
+
value: options.stepped
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function getAggregateConfiguration(node: BaseNode): AggregateConfigurationOptionsEx {
|
|
258
|
+
const nodePriv = node as any;
|
|
259
|
+
|
|
260
|
+
/* istanbul ignore next */
|
|
261
|
+
if (!nodePriv.$historicalDataConfiguration) {
|
|
262
|
+
throw new Error("internal error");
|
|
263
|
+
}
|
|
264
|
+
const aggregateConfiguration = nodePriv.$historicalDataConfiguration.aggregateConfiguration;
|
|
265
|
+
|
|
266
|
+
// Beware ! Stepped value comes from Historical Configuration !
|
|
267
|
+
const stepped = nodePriv.$historicalDataConfiguration.stepped.readValue().value.value;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
percentDataBad: aggregateConfiguration.percentDataBad.readValue().value.value,
|
|
271
|
+
percentDataGood: aggregateConfiguration.percentDataGood.readValue().value.value,
|
|
272
|
+
stepped,
|
|
273
|
+
treatUncertainAsBad: aggregateConfiguration.treatUncertainAsBad.readValue().value.value,
|
|
274
|
+
// xx stepped: aggregateConfiguration.stepped.readValue().value,
|
|
275
|
+
useSlopedExtrapolation: aggregateConfiguration.useSlopedExtrapolation.readValue().value.value
|
|
276
|
+
};
|
|
277
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { UAVariable } from "node-opcua-address-space";
|
|
2
|
+
import { DataValue } from "node-opcua-data-value";
|
|
3
|
+
import { Variant, DataType } from "node-opcua-variant";
|
|
4
|
+
import { getAggregateData } from "./common";
|
|
5
|
+
import { Interval, AggregateConfigurationOptions, isGood } from "./interval";
|
|
6
|
+
import { StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
7
|
+
|
|
8
|
+
function calculateIntervalAverageValue(
|
|
9
|
+
interval: Interval,
|
|
10
|
+
options: AggregateConfigurationOptions
|
|
11
|
+
): DataValue {
|
|
12
|
+
|
|
13
|
+
const indexStart = interval.index;
|
|
14
|
+
let statusCode: StatusCode;
|
|
15
|
+
let isPartial = interval.isPartial;
|
|
16
|
+
|
|
17
|
+
let isRaw = false;
|
|
18
|
+
let hasBad = false;
|
|
19
|
+
|
|
20
|
+
const values: number[] = [];
|
|
21
|
+
|
|
22
|
+
for (let i = indexStart; i < indexStart + interval.count; i++) {
|
|
23
|
+
|
|
24
|
+
const dataValue = interval.dataValues[i];
|
|
25
|
+
|
|
26
|
+
if (dataValue.statusCode === StatusCodes.BadNoData) {
|
|
27
|
+
isPartial = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!isGood(dataValue.statusCode)) {
|
|
32
|
+
hasBad = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
values.push(dataValue.value.value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isRaw) {
|
|
39
|
+
if (hasBad) {
|
|
40
|
+
statusCode = StatusCodes.UncertainDataSubNormal;
|
|
41
|
+
} else {
|
|
42
|
+
statusCode = StatusCodes.Good;
|
|
43
|
+
}
|
|
44
|
+
} else if (hasBad) {
|
|
45
|
+
statusCode = StatusCode.makeStatusCode(StatusCodes.UncertainDataSubNormal, "HistorianCalculated");
|
|
46
|
+
} else {
|
|
47
|
+
statusCode = StatusCode.makeStatusCode(StatusCodes.Good, "HistorianCalculated");
|
|
48
|
+
}
|
|
49
|
+
if (values.length === 0) {
|
|
50
|
+
return new DataValue({
|
|
51
|
+
sourceTimestamp: interval.startTime,
|
|
52
|
+
statusCode: StatusCodes.BadNoData,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const mean = values.reduce((p, c) => p + c, 0) / values.length;
|
|
56
|
+
|
|
57
|
+
return new DataValue({
|
|
58
|
+
sourceTimestamp: interval.startTime,
|
|
59
|
+
statusCode: statusCode as StatusCode,
|
|
60
|
+
value: {
|
|
61
|
+
dataType: DataType.Double, value: mean
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getAverageData(
|
|
67
|
+
node: UAVariable,
|
|
68
|
+
processingInterval: number,
|
|
69
|
+
startDate: Date,
|
|
70
|
+
endDate: Date,
|
|
71
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
72
|
+
) {
|
|
73
|
+
return getAggregateData(node, processingInterval, startDate, endDate, calculateIntervalAverageValue, callback);
|
|
74
|
+
}
|
package/source/common.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opca-aggregates
|
|
3
|
+
*/
|
|
4
|
+
import { SessionContext, UAVariable } from "node-opcua-address-space";
|
|
5
|
+
import { NodeClass } from "node-opcua-data-model";
|
|
6
|
+
import { DataValue } from "node-opcua-data-value";
|
|
7
|
+
import { HistoryData, HistoryReadResult, ReadRawModifiedDetails } from "node-opcua-service-history";
|
|
8
|
+
import { StatusCode } from "node-opcua-status-code";
|
|
9
|
+
|
|
10
|
+
import { getAggregateConfiguration } from "./aggregates";
|
|
11
|
+
import { getInterval, Interval, AggregateConfigurationOptionsEx } from "./interval";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
* @param node
|
|
16
|
+
* @param processingInterval
|
|
17
|
+
* @param startDate
|
|
18
|
+
* @param endDate
|
|
19
|
+
* @param dataValues
|
|
20
|
+
* @param lambda
|
|
21
|
+
* @param callback
|
|
22
|
+
*/
|
|
23
|
+
function processAggregateData(
|
|
24
|
+
node: UAVariable,
|
|
25
|
+
processingInterval: number,
|
|
26
|
+
startDate: Date,
|
|
27
|
+
endDate: Date,
|
|
28
|
+
dataValues: DataValue[],
|
|
29
|
+
lambda: (interval: Interval, aggregateConfiguration: AggregateConfigurationOptionsEx) => DataValue,
|
|
30
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
31
|
+
) {
|
|
32
|
+
const aggregateConfiguration = getAggregateConfiguration(node);
|
|
33
|
+
|
|
34
|
+
const results: DataValue[] = [];
|
|
35
|
+
|
|
36
|
+
const tstart = startDate.getTime();
|
|
37
|
+
const tend = endDate.getTime();
|
|
38
|
+
|
|
39
|
+
const indexHint = 0;
|
|
40
|
+
for (let t = tstart; t < tend; t += processingInterval) {
|
|
41
|
+
const sourceTimestamp = new Date();
|
|
42
|
+
sourceTimestamp.setTime(t);
|
|
43
|
+
|
|
44
|
+
const interval = getInterval(sourceTimestamp, processingInterval, indexHint, dataValues);
|
|
45
|
+
|
|
46
|
+
const dataValue = lambda(interval, aggregateConfiguration);
|
|
47
|
+
|
|
48
|
+
/* istanbul ignore next */
|
|
49
|
+
if (!dataValue || !dataValue.sourceTimestamp) {
|
|
50
|
+
// const dataValue = interval.interpolatedValue(aggregateConfiguration);
|
|
51
|
+
throw Error("invalid DataValue");
|
|
52
|
+
}
|
|
53
|
+
results.push(dataValue);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setImmediate(() => {
|
|
57
|
+
callback(null, results);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getAggregateData(
|
|
63
|
+
node: UAVariable,
|
|
64
|
+
processingInterval: number,
|
|
65
|
+
startDate: Date,
|
|
66
|
+
endDate: Date,
|
|
67
|
+
lambda: (interval: Interval, aggregateConfiguration: AggregateConfigurationOptionsEx) => DataValue,
|
|
68
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
69
|
+
) {
|
|
70
|
+
|
|
71
|
+
/* istanbul ignore next */
|
|
72
|
+
if (node.nodeClass !== NodeClass.Variable) {
|
|
73
|
+
throw new Error("node must be UAVariable");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* istanbul ignore next */
|
|
77
|
+
if (processingInterval <= 0) {
|
|
78
|
+
throw new Error("Invalid processing interval, shall be greater than 0");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const context = new SessionContext();
|
|
82
|
+
const historyReadDetails = new ReadRawModifiedDetails({
|
|
83
|
+
endTime: endDate,
|
|
84
|
+
startTime: startDate,
|
|
85
|
+
});
|
|
86
|
+
const indexRange = null;
|
|
87
|
+
const dataEncoding = null;
|
|
88
|
+
const continuationPoint = null;
|
|
89
|
+
node.historyRead(context, historyReadDetails, indexRange, dataEncoding, continuationPoint,
|
|
90
|
+
(err: Error | null, result?: HistoryReadResult) => {
|
|
91
|
+
|
|
92
|
+
/* istanbul ignore next */
|
|
93
|
+
if (err) {
|
|
94
|
+
return callback(err);
|
|
95
|
+
}
|
|
96
|
+
const historyData = result!.historyData as HistoryData;
|
|
97
|
+
|
|
98
|
+
const dataValues = historyData.dataValues || [];
|
|
99
|
+
|
|
100
|
+
processAggregateData(node, processingInterval, startDate, endDate, dataValues, lambda, callback);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function interpolateValue(dataValue1: DataValue, dataValue2: DataValue, date: Date) {
|
|
105
|
+
const t0 = dataValue1.sourceTimestamp!.getTime();
|
|
106
|
+
const t = date.getTime();
|
|
107
|
+
const t1 = dataValue2.sourceTimestamp!.getTime();
|
|
108
|
+
const coef1 = (t - t0) / (t1 - t0);
|
|
109
|
+
const coef2 = (t1 - t) / (t1 - t0);
|
|
110
|
+
const value = dataValue1.value.clone();
|
|
111
|
+
value.value = coef2 * dataValue1.value.value + coef1 * dataValue2.value.value;
|
|
112
|
+
const statusCode = StatusCode.makeStatusCode(dataValue1.statusCode, "HistorianInterpolated");
|
|
113
|
+
return new DataValue({
|
|
114
|
+
sourceTimestamp: date,
|
|
115
|
+
statusCode,
|
|
116
|
+
value
|
|
117
|
+
});
|
|
118
|
+
}
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opca-aggregates
|
|
3
|
+
*/
|
|
4
|
+
export {
|
|
5
|
+
addAggregateSupport,
|
|
6
|
+
installAggregateConfigurationOptions,
|
|
7
|
+
getAggregateConfiguration,
|
|
8
|
+
} from "./aggregates";
|
|
9
|
+
export * from "./interpolate";
|
|
10
|
+
export * from "./minmax";
|
|
11
|
+
export * from "./interval";
|
|
12
|
+
export * from "./common";
|
|
13
|
+
export * from "./average";
|
|
14
|
+
export * from "./read_processed_details";
|
|
15
|
+
export {
|
|
16
|
+
AggregateFunction
|
|
17
|
+
} from "node-opcua-constants";
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opca-aggregates
|
|
3
|
+
*/
|
|
4
|
+
// excerpt from OPC Unified Architecture, Part 13 21 Release 1.04
|
|
5
|
+
//
|
|
6
|
+
// 5.4.3.4 Interpolative
|
|
7
|
+
// The Interpolative Aggregate defined in Table 15 returns the Interpolated Bounding Value for the startTime
|
|
8
|
+
// of each interval.
|
|
9
|
+
// When searching for Good values before or after the bounding value, the time period searched is Server specific,
|
|
10
|
+
// but the Server should search a time range which is at least the size of the ProcessingInterval.
|
|
11
|
+
//
|
|
12
|
+
// Interpolated Aggregate Characteristics
|
|
13
|
+
//
|
|
14
|
+
// Type Interpolated
|
|
15
|
+
// Data Type Same as Source
|
|
16
|
+
// Use Bounds Interpolated
|
|
17
|
+
// Timestamp StartTime
|
|
18
|
+
// Status Code Calculations
|
|
19
|
+
// Calculation Method Custom
|
|
20
|
+
// Good if no Bad values skipped and Good values are used,
|
|
21
|
+
// Uncertain if Bad values skipped or if Uncertain values are used. If
|
|
22
|
+
// no starting value then BadNoData.
|
|
23
|
+
// Partial bit Not Set
|
|
24
|
+
// Calculated bit Not Set
|
|
25
|
+
// Interpolated bit Set Sometimes
|
|
26
|
+
// Always set except for when the Raw bit is set
|
|
27
|
+
// Raw bit Set Sometimes
|
|
28
|
+
// If a value exists with the exact time of interval Start
|
|
29
|
+
// Multi Value bit Not Set
|
|
30
|
+
//
|
|
31
|
+
// Status Code Common Special Cases
|
|
32
|
+
// Before Start of Data Return BadNoData
|
|
33
|
+
// After End of Data Return extrapolated value (see 3.1.8) (sloped or stepped according to settings)
|
|
34
|
+
// Status code is Uncertain_DataSubNormal.
|
|
35
|
+
// Start Bound Not Found BadNoData.
|
|
36
|
+
// End Bound Not Found See “After End of Data”
|
|
37
|
+
// Bound Bad Does not return a Bad bound except as noted above
|
|
38
|
+
// Bound Uncertain Returned Uncertain_DataSubNormal if any Bad value(s) was/were skipped to
|
|
39
|
+
// calculate the bounding value.
|
|
40
|
+
import { UAVariable } from "node-opcua-address-space";
|
|
41
|
+
import { assert } from "node-opcua-assert";
|
|
42
|
+
import { DataValue } from "node-opcua-data-value";
|
|
43
|
+
import { StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
44
|
+
|
|
45
|
+
import { getAggregateData, interpolateValue } from "./common";
|
|
46
|
+
import {
|
|
47
|
+
_findGoodDataValueBefore,
|
|
48
|
+
adjustProcessingOptions,
|
|
49
|
+
AggregateConfigurationOptionsEx,
|
|
50
|
+
Interval, isBad,
|
|
51
|
+
isGood
|
|
52
|
+
} from "./interval";
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
For any intervals containing regions where the StatusCodes are Bad,
|
|
56
|
+
the total duration of all Bad regions is calculated and divided by the width of the interval.
|
|
57
|
+
The resulting ratio is multiplied by 100 and compared to the PercentDataBad parameter.
|
|
58
|
+
The StatusCode for the interval is Bad if the ratio is greater than or equal to the PercentDataBad parameter.
|
|
59
|
+
For any interval which is not Bad, the total duration of all Good regions is then calculated and divided by
|
|
60
|
+
the width of the interval. The resulting ratio is multiplied by 100 and compared to the PercentDataGood parameter.
|
|
61
|
+
The StatusCode for the interval is Good if the ratio is greater than or equal to the PercentDataGood parameter.
|
|
62
|
+
If for an interval neither ratio applies then that interval is Uncertain_DataSubNormal.
|
|
63
|
+
*/
|
|
64
|
+
export function interpolatedValue(interval: Interval, options: AggregateConfigurationOptionsEx): DataValue {
|
|
65
|
+
|
|
66
|
+
options = adjustProcessingOptions(options);
|
|
67
|
+
|
|
68
|
+
assert(Object.prototype.hasOwnProperty.call(options,"useSlopedExtrapolation"));
|
|
69
|
+
assert(Object.prototype.hasOwnProperty.call(options,"treatUncertainAsBad"));
|
|
70
|
+
|
|
71
|
+
const bTreatUncertainAsBad = options.treatUncertainAsBad!;
|
|
72
|
+
|
|
73
|
+
const steppedValue = (previousDataValue: DataValue): DataValue => {
|
|
74
|
+
if (!previousDataValue.statusCode) {
|
|
75
|
+
throw new Error("Expecting statusCode");
|
|
76
|
+
}
|
|
77
|
+
const interpValue = new DataValue({
|
|
78
|
+
sourceTimestamp: interval.startTime,
|
|
79
|
+
statusCode: StatusCodes.Bad,
|
|
80
|
+
value: previousDataValue.value,
|
|
81
|
+
});
|
|
82
|
+
interpValue.statusCode =
|
|
83
|
+
StatusCode.makeStatusCode(StatusCodes.UncertainDataSubNormal, "HistorianInterpolated");
|
|
84
|
+
return interpValue;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (interval.index === -1) {
|
|
88
|
+
// the interval is beyond end Data
|
|
89
|
+
// we need to find previous good value
|
|
90
|
+
// and second previous good value to extrapolate
|
|
91
|
+
const prev1 = _findGoodDataValueBefore(interval.dataValues, interval.dataValues.length, bTreatUncertainAsBad);
|
|
92
|
+
if (prev1.index <= 0) {
|
|
93
|
+
return new DataValue({
|
|
94
|
+
sourceTimestamp: interval.startTime,
|
|
95
|
+
statusCode: StatusCodes.BadNoData,
|
|
96
|
+
value: undefined,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!options.useSlopedExtrapolation) {
|
|
100
|
+
return steppedValue(prev1.dataValue);
|
|
101
|
+
}
|
|
102
|
+
const prev2 = _findGoodDataValueBefore(interval.dataValues, prev1.index, bTreatUncertainAsBad);
|
|
103
|
+
|
|
104
|
+
if (prev2.index <= 0) {
|
|
105
|
+
// use step value
|
|
106
|
+
return steppedValue(prev1.dataValue);
|
|
107
|
+
}
|
|
108
|
+
// else interpolate
|
|
109
|
+
const interpVal = interpolateValue(prev2.dataValue, prev1.dataValue, interval.startTime);
|
|
110
|
+
|
|
111
|
+
// tslint:disable:no-bitwise
|
|
112
|
+
if (prev2.index + 1 < prev1.index || prev1.index < interval.dataValues.length - 1) {
|
|
113
|
+
// some bad data exist in between = change status code
|
|
114
|
+
const mask = 0x0000FFFFFF;
|
|
115
|
+
const extraBits = interpVal.statusCode.value & mask;
|
|
116
|
+
interpVal.statusCode = StatusCode.makeStatusCode(StatusCodes.UncertainDataSubNormal, extraBits);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return interpVal;
|
|
120
|
+
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* istanbul ignore next */
|
|
124
|
+
if (interval.index < 0 && interval.count === 0) {
|
|
125
|
+
return new DataValue({
|
|
126
|
+
sourceTimestamp: interval.startTime,
|
|
127
|
+
statusCode: StatusCodes.BadNoData
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dataValue1 = interval.dataValues[interval.index];
|
|
132
|
+
|
|
133
|
+
// if a non-Bad Raw value exists at the timestamp then it is the bounding value;
|
|
134
|
+
if (!isBad(dataValue1.statusCode) && interval.hasRawDataAsStart()) {
|
|
135
|
+
return dataValue1;
|
|
136
|
+
}
|
|
137
|
+
// find the first non-Bad Raw value before the timestamp;
|
|
138
|
+
|
|
139
|
+
// find previous good value
|
|
140
|
+
const before = interval.beforeStartDataValue(bTreatUncertainAsBad);
|
|
141
|
+
if (isBad(before.dataValue.statusCode)) {
|
|
142
|
+
return new DataValue({
|
|
143
|
+
sourceTimestamp: interval.startTime,
|
|
144
|
+
statusCode: StatusCodes.BadNoData
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (options.stepped) {
|
|
149
|
+
if (before.index + 1 === interval.index) {
|
|
150
|
+
return new DataValue({
|
|
151
|
+
sourceTimestamp: interval.startTime,
|
|
152
|
+
statusCode: StatusCode.makeStatusCode(before.dataValue.statusCode, "HistorianInterpolated"),
|
|
153
|
+
value: before.dataValue.value
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return steppedValue(before.dataValue);
|
|
157
|
+
}
|
|
158
|
+
// find the first non-Bad Raw value after the timestamp;
|
|
159
|
+
const next = interval.nextStartDataValue(bTreatUncertainAsBad);
|
|
160
|
+
|
|
161
|
+
// draw a line between before value and after value;
|
|
162
|
+
// use point where the line crosses the timestamp as an estimate of the bounding value.
|
|
163
|
+
// The calculation can be expressed with the following formula:
|
|
164
|
+
// V bound = (T bound – T before)x( V after – V before)/( T after – T before) + V before
|
|
165
|
+
// where V
|
|
166
|
+
// x is a value at ‘x’ and Tx is the timestamp associated with Vx.
|
|
167
|
+
const interpolatedDataValue = interpolateValue(before.dataValue, next.dataValue, interval.startTime);
|
|
168
|
+
|
|
169
|
+
if (before.index + 1 < next.index
|
|
170
|
+
|| !isGood(next.dataValue.statusCode)
|
|
171
|
+
|| !isGood(before.dataValue.statusCode)
|
|
172
|
+
) {
|
|
173
|
+
// tslint:disable:no-bitwise
|
|
174
|
+
// some bad data exist in between = change status code
|
|
175
|
+
const mask = 0x0000FFFFFF;
|
|
176
|
+
const extraBits = interpolatedDataValue.statusCode.value & mask;
|
|
177
|
+
interpolatedDataValue.statusCode =
|
|
178
|
+
StatusCode.makeStatusCode(StatusCodes.UncertainDataSubNormal, extraBits);
|
|
179
|
+
}
|
|
180
|
+
// check if uncertain or bad value exist between before/next
|
|
181
|
+
// todo
|
|
182
|
+
return interpolatedDataValue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
*
|
|
187
|
+
* @param node
|
|
188
|
+
* @param processingInterval
|
|
189
|
+
* @param startDate
|
|
190
|
+
* @param endDate
|
|
191
|
+
* @param callback
|
|
192
|
+
*/
|
|
193
|
+
export function getInterpolatedData(
|
|
194
|
+
node: UAVariable,
|
|
195
|
+
processingInterval: number,
|
|
196
|
+
startDate: Date,
|
|
197
|
+
endDate: Date,
|
|
198
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
199
|
+
) {
|
|
200
|
+
return getAggregateData(node, processingInterval, startDate, endDate, interpolatedValue, callback);
|
|
201
|
+
}
|