node-opcua-aggregates 2.76.2 → 2.78.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/dist/aggregates.d.ts +19 -4
- package/dist/aggregates.js +95 -20
- package/dist/aggregates.js.map +1 -1
- package/dist/calculate_bad_good.d.ts +10 -0
- package/dist/calculate_bad_good.js +125 -0
- package/dist/calculate_bad_good.js.map +1 -0
- package/dist/common.js +1 -7
- package/dist/common.js.map +1 -1
- package/dist/count.d.ts +3 -0
- package/dist/count.js +87 -0
- package/dist/count.js.map +1 -0
- package/dist/duration_bad.d.ts +4 -0
- package/dist/duration_bad.js +33 -0
- package/dist/duration_bad.js.map +1 -0
- package/dist/duration_good.d.ts +4 -0
- package/dist/duration_good.js +33 -0
- package/dist/duration_good.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/interval.d.ts +17 -1
- package/dist/interval.js +52 -5
- package/dist/interval.js.map +1 -1
- package/dist/percent_bad.d.ts +4 -0
- package/dist/percent_bad.js +26 -0
- package/dist/percent_bad.js.map +1 -0
- package/dist/percent_good.d.ts +7 -0
- package/dist/percent_good.js +50 -0
- package/dist/percent_good.js.map +1 -0
- package/dist/read_processed_details.js +44 -19
- package/dist/read_processed_details.js.map +1 -1
- package/package.json +18 -18
- package/source/aggregates.ts +118 -62
- package/source/calculate_bad_good.ts +148 -0
- package/source/common.ts +3 -9
- package/source/count.ts +98 -0
- package/source/duration_bad.ts +37 -0
- package/source/duration_good.ts +37 -0
- package/source/index.ts +7 -1
- package/source/interval.ts +53 -4
- package/source/percent_bad.ts +30 -0
- package/source/percent_good.ts +55 -0
- package/source/read_processed_details.ts +44 -23
package/source/aggregates.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opcua-aggregates
|
|
3
3
|
*/
|
|
4
|
-
import { AggregateFunction } from "node-opcua-constants";
|
|
5
|
-
import { makeNodeId } from "node-opcua-nodeid";
|
|
4
|
+
import { AggregateFunction, ObjectIds, ObjectTypeIds, ReferenceTypeIds } from "node-opcua-constants";
|
|
5
|
+
import { coerceNodeId, makeNodeId, NodeId, NodeIdLike, resolveNodeId, sameNodeId } from "node-opcua-nodeid";
|
|
6
6
|
import * as utils from "node-opcua-utils";
|
|
7
7
|
import { DataType } from "node-opcua-variant";
|
|
8
8
|
import {
|
|
9
9
|
AddressSpace,
|
|
10
10
|
BaseNode,
|
|
11
|
+
IAddressSpace,
|
|
12
|
+
UAHistoricalDataConfiguration,
|
|
11
13
|
UAHistoryServerCapabilities,
|
|
12
14
|
UAObject,
|
|
13
15
|
UAServerCapabilities,
|
|
14
16
|
UAVariable
|
|
15
17
|
} from "node-opcua-address-space";
|
|
16
18
|
import { AddressSpacePrivate } from "node-opcua-address-space/src/address_space_private";
|
|
19
|
+
import { BrowseDirection, coerceQualifiedName, NodeClass, NodeClassMask } from "node-opcua-data-model";
|
|
20
|
+
import { assert } from "node-opcua-assert";
|
|
17
21
|
|
|
18
22
|
import { AggregateConfigurationOptionsEx } from "./interval";
|
|
19
23
|
import { readProcessedDetails } from "./read_processed_details";
|
|
@@ -121,53 +125,11 @@ function setHistoricalServerCapabilities(historyServerCapabilities: any, default
|
|
|
121
125
|
// xx setBoolean("InsertAnnotationsCapability");
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
export type AggregateFunctionName =
|
|
125
|
-
| "AnnotationCount"
|
|
126
|
-
| "Average"
|
|
127
|
-
| "Count"
|
|
128
|
-
| "Delta"
|
|
129
|
-
| "DeltaBounds"
|
|
130
|
-
| "DurationBad"
|
|
131
|
-
| "DurationGood"
|
|
132
|
-
| "DurationInStateNonZero"
|
|
133
|
-
| "DurationInStateZero"
|
|
134
|
-
| "EndBound"
|
|
135
|
-
| "Interpolative"
|
|
136
|
-
| "Maximum"
|
|
137
|
-
| "Maximum2"
|
|
138
|
-
| "MaximumActualTime"
|
|
139
|
-
| "MaximumActualTime2"
|
|
140
|
-
| "Minimum"
|
|
141
|
-
| "Minimum2"
|
|
142
|
-
| "MinimumActualTime"
|
|
143
|
-
| "MinimumActualTime2"
|
|
144
|
-
| "NumberOfTransitions"
|
|
145
|
-
| "PercentBad"
|
|
146
|
-
| "PercentGood"
|
|
147
|
-
| "Range"
|
|
148
|
-
| "Range2"
|
|
149
|
-
| "StandardDeviationPopulation"
|
|
150
|
-
| "StandardDeviationSample"
|
|
151
|
-
| "Start"
|
|
152
|
-
| "StartBound"
|
|
153
|
-
| "TimeAverage"
|
|
154
|
-
| "TimeAverage2"
|
|
155
|
-
| "Total"
|
|
156
|
-
| "Total2"
|
|
157
|
-
| "VariancePopulation"
|
|
158
|
-
| "VarianceSample"
|
|
159
|
-
| "WorstQuality"
|
|
160
|
-
| "WorstQuality2";
|
|
161
|
-
|
|
162
128
|
interface UAHistoryServerCapabilitiesWithH extends UAServerCapabilities {
|
|
163
129
|
historyServerCapabilities: UAHistoryServerCapabilities;
|
|
164
130
|
}
|
|
165
|
-
function addAggregateFunctionSupport(addressSpace: AddressSpace, functionName: number): void {
|
|
166
|
-
/* istanbul ignore next */
|
|
167
|
-
if (!functionName) {
|
|
168
|
-
throw new Error("Invalid function name");
|
|
169
|
-
}
|
|
170
131
|
|
|
132
|
+
export function addAggregateFunctionSupport(addressSpace: AddressSpace, aggregateFunctionNodeId: NodeIdLike): void {
|
|
171
133
|
const serverCapabilities = addressSpace.rootFolder.objects.server.serverCapabilities as UAHistoryServerCapabilitiesWithH;
|
|
172
134
|
|
|
173
135
|
/* istanbul ignore next */
|
|
@@ -179,12 +141,19 @@ function addAggregateFunctionSupport(addressSpace: AddressSpace, functionName: n
|
|
|
179
141
|
|
|
180
142
|
const aggregateFunctionsInHist = serverCapabilities.historyServerCapabilities.aggregateFunctions;
|
|
181
143
|
|
|
182
|
-
const
|
|
183
|
-
const functionNode = addressSpace.getNamespace(0).findNode(functionNodeId);
|
|
144
|
+
const functionNode = addressSpace.findNode(aggregateFunctionNodeId);
|
|
184
145
|
|
|
185
146
|
/* istanbul ignore next */
|
|
186
147
|
if (!functionNode) {
|
|
187
|
-
throw new Error("Cannot find node " +
|
|
148
|
+
throw new Error("Cannot find node " + aggregateFunctionNodeId.toString() + " in addressSpace");
|
|
149
|
+
}
|
|
150
|
+
/* istanbul ignore next */
|
|
151
|
+
if (functionNode.nodeClass !== NodeClass.Object) {
|
|
152
|
+
throw new Error("Expecting an object Node");
|
|
153
|
+
}
|
|
154
|
+
/* istanbul ignore next */
|
|
155
|
+
if (!sameNodeId((functionNode as UAObject).typeDefinition, coerceNodeId(ObjectTypeIds.AggregateFunctionType))) {
|
|
156
|
+
throw new Error("Expecting an object with TypeDefinition AggregateFunctionType");
|
|
188
157
|
}
|
|
189
158
|
|
|
190
159
|
aggregateFunctions.addReference({
|
|
@@ -197,7 +166,23 @@ function addAggregateFunctionSupport(addressSpace: AddressSpace, functionName: n
|
|
|
197
166
|
});
|
|
198
167
|
}
|
|
199
168
|
|
|
200
|
-
export function
|
|
169
|
+
export function addAggregateStandardFunctionSupport(addressSpace: AddressSpace, functionName: AggregateFunction): void {
|
|
170
|
+
/* istanbul ignore next */
|
|
171
|
+
if (!functionName) {
|
|
172
|
+
throw new Error("Invalid function name");
|
|
173
|
+
}
|
|
174
|
+
const functionNodeId = makeNodeId(functionName);
|
|
175
|
+
addAggregateFunctionSupport(addressSpace, functionNodeId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function addAggregateSupport(addressSpace: AddressSpace, aggregatedFunctions?: AggregateFunction[]): void {
|
|
179
|
+
aggregatedFunctions = aggregatedFunctions || [
|
|
180
|
+
AggregateFunction.Interpolative,
|
|
181
|
+
AggregateFunction.Minimum,
|
|
182
|
+
AggregateFunction.Maximum,
|
|
183
|
+
AggregateFunction.Average
|
|
184
|
+
];
|
|
185
|
+
|
|
201
186
|
const aggregateConfigurationType = addressSpace.getNamespace(0).findObjectType("AggregateConfigurationType");
|
|
202
187
|
|
|
203
188
|
/* istanbul ignore next */
|
|
@@ -232,37 +217,108 @@ export function addAggregateSupport(addressSpace: AddressSpace): void {
|
|
|
232
217
|
|
|
233
218
|
setHistoricalServerCapabilities(historyServerCapabilities, historicalCapabilitiesDefaultProperties);
|
|
234
219
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
addAggregateFunctionSupport(addressSpace, AggregateFunction.Average);
|
|
239
|
-
|
|
220
|
+
for (const f of aggregatedFunctions) {
|
|
221
|
+
addAggregateStandardFunctionSupport(addressSpace, f);
|
|
222
|
+
}
|
|
240
223
|
const addressSpaceInternal = addressSpace as unknown as AddressSpacePrivate;
|
|
241
224
|
addressSpaceInternal._readProcessedDetails = readProcessedDetails;
|
|
242
225
|
}
|
|
243
226
|
|
|
244
|
-
|
|
245
|
-
|
|
227
|
+
interface BaseNodeWithHistoricalDataConfiguration extends UAVariable {
|
|
228
|
+
$historicalDataConfiguration: UAHistoricalDataConfiguration;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function getAggregateFunctions(addressSpace: IAddressSpace): NodeId[] {
|
|
232
|
+
const aggregateFunctionTypeNodeId = resolveNodeId(ObjectTypeIds.AggregateFunctionType);
|
|
233
|
+
const aggregateFunctions = addressSpace.findNode(ObjectIds.Server_ServerCapabilities_AggregateFunctions) as UAObject;
|
|
234
|
+
if (!aggregateFunctions) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const referenceDescripitions = aggregateFunctions.browseNode({
|
|
238
|
+
referenceTypeId: ReferenceTypeIds.HierarchicalReferences,
|
|
239
|
+
resultMask: 63,
|
|
240
|
+
nodeClassMask: NodeClassMask.Object,
|
|
241
|
+
browseDirection: BrowseDirection.Forward,
|
|
242
|
+
includeSubtypes: true
|
|
243
|
+
});
|
|
244
|
+
const aggregateFunctionsNodeIds = referenceDescripitions
|
|
245
|
+
.filter((a) => sameNodeId(a.typeDefinition, aggregateFunctionTypeNodeId))
|
|
246
|
+
.map((a) => a.nodeId);
|
|
247
|
+
return aggregateFunctionsNodeIds;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Install aggregateConfiguration on an historizing variable
|
|
252
|
+
*
|
|
253
|
+
* @param node the variable on which to add the aggregateConfiguration.
|
|
254
|
+
* @param options the default AggregateConfigurationOptions.
|
|
255
|
+
* @param aggregateFunctions the aggregatedFunctions, if not specified the aggregatedFunction of ServerCapabilities.AggregatedFunction will be used.
|
|
256
|
+
|
|
257
|
+
*/
|
|
258
|
+
export function installAggregateConfigurationOptions(
|
|
259
|
+
node: UAVariable,
|
|
260
|
+
options: AggregateConfigurationOptionsEx,
|
|
261
|
+
aggregateFunctions?: NodeIdLike[]
|
|
262
|
+
): void {
|
|
263
|
+
const nodePriv = node as BaseNodeWithHistoricalDataConfiguration;
|
|
264
|
+
|
|
265
|
+
// istanbul ignore next
|
|
266
|
+
if (!nodePriv.historizing) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"variable.historizing is not set\n make sure addressSpace.installHistoricalDataNode(variable) has been called"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
const aggregateConfiguration = nodePriv.$historicalDataConfiguration.aggregateConfiguration;
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
|
|
274
|
+
const f = (a: number | boolean | undefined, defaultValue: number | boolean): number | boolean =>
|
|
275
|
+
a === undefined ? defaultValue : a;
|
|
276
|
+
|
|
277
|
+
aggregateConfiguration.percentDataBad.setValueFromSource({ dataType: "Byte", value: f(options.percentDataBad, 100) });
|
|
278
|
+
aggregateConfiguration.percentDataGood.setValueFromSource({ dataType: "Byte", value: f(options.percentDataGood, 100) });
|
|
249
279
|
aggregateConfiguration.treatUncertainAsBad.setValueFromSource({
|
|
250
280
|
dataType: "Boolean",
|
|
251
|
-
value: options.treatUncertainAsBad
|
|
281
|
+
value: f(options.treatUncertainAsBad, false)
|
|
252
282
|
});
|
|
253
283
|
aggregateConfiguration.useSlopedExtrapolation.setValueFromSource({
|
|
254
284
|
dataType: "Boolean",
|
|
255
|
-
value: options.useSlopedExtrapolation
|
|
285
|
+
value: f(options.useSlopedExtrapolation, false)
|
|
256
286
|
});
|
|
257
287
|
|
|
258
288
|
nodePriv.$historicalDataConfiguration.stepped.setValueFromSource({
|
|
259
289
|
dataType: "Boolean",
|
|
260
|
-
value: options.stepped
|
|
290
|
+
value: f(options.stepped, false)
|
|
261
291
|
});
|
|
292
|
+
// https://reference.opcfoundation.org/v104/Core/docs/Part13/4.4/
|
|
293
|
+
// Exposing Supported Functions and Capabilities
|
|
294
|
+
if (!aggregateFunctions) {
|
|
295
|
+
aggregateFunctions = getAggregateFunctions(node.addressSpace);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let uaAggregateFunctions = nodePriv.$historicalDataConfiguration.aggregateFunctions;
|
|
299
|
+
if (!uaAggregateFunctions) {
|
|
300
|
+
const namespace = nodePriv.namespace;
|
|
301
|
+
uaAggregateFunctions = namespace.addObject({
|
|
302
|
+
browseName: coerceQualifiedName({ name: "AggregateFunctions", namespaceIndex: 0 }),
|
|
303
|
+
componentOf: nodePriv.$historicalDataConfiguration
|
|
304
|
+
});
|
|
305
|
+
uaAggregateFunctions = nodePriv.$historicalDataConfiguration.aggregateFunctions;
|
|
306
|
+
}
|
|
307
|
+
// verify that all aggregateFunctions are of type AggregateFunctionType
|
|
308
|
+
// ... to do
|
|
309
|
+
|
|
310
|
+
const referenceType = resolveNodeId(ReferenceTypeIds.Organizes);
|
|
311
|
+
for (const nodeId of aggregateFunctions) {
|
|
312
|
+
uaAggregateFunctions!.addReference({
|
|
313
|
+
nodeId,
|
|
314
|
+
referenceType,
|
|
315
|
+
isForward: true
|
|
316
|
+
});
|
|
317
|
+
}
|
|
262
318
|
}
|
|
263
319
|
|
|
264
320
|
export function getAggregateConfiguration(node: BaseNode): AggregateConfigurationOptionsEx {
|
|
265
|
-
const nodePriv = node as
|
|
321
|
+
const nodePriv = node as BaseNodeWithHistoricalDataConfiguration;
|
|
266
322
|
|
|
267
323
|
/* istanbul ignore next */
|
|
268
324
|
if (!nodePriv.$historicalDataConfiguration) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { extraStatusCodeBits, StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
2
|
+
|
|
3
|
+
import { Interval, AggregateConfigurationOptions, isUncertain, isBad, isGoodish2, isGoodish } from "./interval";
|
|
4
|
+
|
|
5
|
+
// function isBadWithUncertain(statusCode: StatusCode, treatUncertainAsBad?: boolean): boolean {
|
|
6
|
+
// if (isGoodish(statusCode)) return false;
|
|
7
|
+
|
|
8
|
+
// if (isUncertain(statusCode)) {
|
|
9
|
+
// return treatUncertainAsBad || false;
|
|
10
|
+
// }
|
|
11
|
+
// return true;
|
|
12
|
+
// }
|
|
13
|
+
|
|
14
|
+
const a = (s: StatusCode | undefined, options: AggregateConfigurationOptions) =>
|
|
15
|
+
!s || s === StatusCodes.BadNoData
|
|
16
|
+
? StatusCodes.BadNoData
|
|
17
|
+
: isBad(s) || (options.treatUncertainAsBad && isUncertain(s))
|
|
18
|
+
? StatusCodes.Bad
|
|
19
|
+
: StatusCodes.Good;
|
|
20
|
+
|
|
21
|
+
function findLowBound(
|
|
22
|
+
interval: Interval,
|
|
23
|
+
options: AggregateConfigurationOptions
|
|
24
|
+
): { previousStatus: StatusCode; previousTime: number; indexStart: number } {
|
|
25
|
+
const indexStart: number = interval.index;
|
|
26
|
+
|
|
27
|
+
const initialValue = interval.dataValues[indexStart];
|
|
28
|
+
if (initialValue.sourceTimestamp!.getTime() === interval.startTime.getTime()) {
|
|
29
|
+
return { previousStatus: initialValue.statusCode, previousTime: interval.startTime.getTime(), indexStart: indexStart + 1 };
|
|
30
|
+
}
|
|
31
|
+
const previousStatus =
|
|
32
|
+
indexStart === 0 || !interval.dataValues[indexStart - 1]
|
|
33
|
+
? StatusCodes.BadNoData
|
|
34
|
+
: a(interval.dataValues[indexStart - 1].statusCode, options);
|
|
35
|
+
const previousTime = interval.startTime.getTime();
|
|
36
|
+
return { previousStatus, previousTime, indexStart };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// eslint-disable-next-line max-statements, complexity
|
|
40
|
+
export function calculateBadAndGood(
|
|
41
|
+
interval: Interval,
|
|
42
|
+
options: AggregateConfigurationOptions
|
|
43
|
+
): {
|
|
44
|
+
durationGood: number;
|
|
45
|
+
durationBad: number;
|
|
46
|
+
durationUnknown: number;
|
|
47
|
+
percentBad: number;
|
|
48
|
+
percentGood: number;
|
|
49
|
+
statusCode: StatusCode;
|
|
50
|
+
} {
|
|
51
|
+
if (interval.count === 0) {
|
|
52
|
+
return {
|
|
53
|
+
durationGood: 0,
|
|
54
|
+
durationBad: 0,
|
|
55
|
+
durationUnknown: 0,
|
|
56
|
+
percentBad: 0,
|
|
57
|
+
percentGood: 0,
|
|
58
|
+
statusCode: StatusCodes.BadNoData
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
let durationGood = 0;
|
|
62
|
+
let durationBad = 0;
|
|
63
|
+
let durationUnknown = 0;
|
|
64
|
+
|
|
65
|
+
let partialFlag = interval.isPartial ? extraStatusCodeBits.HistorianPartial : 0;
|
|
66
|
+
|
|
67
|
+
let { previousStatus, previousTime, indexStart } = findLowBound(interval, options);
|
|
68
|
+
|
|
69
|
+
if (previousStatus === StatusCodes.BadNoData) {
|
|
70
|
+
partialFlag = extraStatusCodeBits.HistorianPartial;
|
|
71
|
+
previousStatus = StatusCodes.Bad;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let nbGood = 0;
|
|
75
|
+
let nbBad = 0;
|
|
76
|
+
let nbUncertain = 0;
|
|
77
|
+
indexStart += 0;
|
|
78
|
+
for (let i = indexStart; i < interval.index + interval.count; i++) {
|
|
79
|
+
const dataValue = interval.dataValues[i];
|
|
80
|
+
if (isGoodish(dataValue.statusCode)) {
|
|
81
|
+
nbGood++;
|
|
82
|
+
}
|
|
83
|
+
if (isUncertain(dataValue.statusCode)) {
|
|
84
|
+
nbUncertain++;
|
|
85
|
+
}
|
|
86
|
+
if (isBad(dataValue.statusCode)) {
|
|
87
|
+
nbBad++;
|
|
88
|
+
}
|
|
89
|
+
const currentStatus = a(dataValue.statusCode, options);
|
|
90
|
+
if (currentStatus === StatusCodes.BadNoData) {
|
|
91
|
+
partialFlag = extraStatusCodeBits.HistorianPartial;
|
|
92
|
+
}
|
|
93
|
+
const currentTime = dataValue.sourceTimestamp!.getTime();
|
|
94
|
+
|
|
95
|
+
// console.log(" ", dataValue.sourceTimestamp?.toISOString(), dataValue.statusCode.toString(), dataValue.value.value);
|
|
96
|
+
|
|
97
|
+
if (currentStatus === previousStatus) continue;
|
|
98
|
+
if (previousStatus === StatusCodes.Good) {
|
|
99
|
+
// if (isBadWithUncertain(currentStatus, options.treatUncertainAsBad)) {
|
|
100
|
+
// durationBad += currentTime - previousTime;
|
|
101
|
+
// } else {
|
|
102
|
+
durationGood += currentTime - previousTime;
|
|
103
|
+
// }
|
|
104
|
+
} else if (previousStatus === StatusCodes.BadNoData) {
|
|
105
|
+
durationUnknown += currentTime - previousTime;
|
|
106
|
+
} else {
|
|
107
|
+
durationBad += currentTime - previousTime;
|
|
108
|
+
}
|
|
109
|
+
previousStatus = currentStatus;
|
|
110
|
+
previousTime = currentTime;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// final step
|
|
114
|
+
const currentTime = interval.getEffectiveEndTime();
|
|
115
|
+
if (previousStatus === StatusCodes.Good) {
|
|
116
|
+
durationGood += currentTime - previousTime;
|
|
117
|
+
} else if (previousStatus === StatusCodes.BadNoData) {
|
|
118
|
+
durationUnknown += currentTime - previousTime;
|
|
119
|
+
} else {
|
|
120
|
+
durationBad += currentTime - previousTime;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (nbGood === 0) {
|
|
124
|
+
if (nbBad > 0) {
|
|
125
|
+
// we need at lest a Good Status in the intervale to be good & no bad !
|
|
126
|
+
durationGood = -1;
|
|
127
|
+
} else {
|
|
128
|
+
durationGood = 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const effectiveProcessingInterval = currentTime - interval.startTime.getTime();
|
|
132
|
+
|
|
133
|
+
const percentGood = (durationGood / effectiveProcessingInterval) * 100;
|
|
134
|
+
const percentBad = (durationBad / effectiveProcessingInterval) * 100;
|
|
135
|
+
|
|
136
|
+
let percentDataGood = options.percentDataGood === undefined ? 100 : options.percentDataGood;
|
|
137
|
+
const percentDataBad = options.percentDataBad === undefined ? 100 : options.percentDataBad;
|
|
138
|
+
|
|
139
|
+
if (percentBad >= percentDataBad || (nbGood === 0 && nbUncertain === 0)) {
|
|
140
|
+
durationGood = 0; // BAD
|
|
141
|
+
percentDataGood = -1;
|
|
142
|
+
// const statusCode = StatusCodes.Bad;
|
|
143
|
+
//return { durationGood, durationBad, durationUnknown, percentBad, percentGood, statusCode };
|
|
144
|
+
}
|
|
145
|
+
const statusCode = StatusCode.makeStatusCode(StatusCodes.Good, extraStatusCodeBits.HistorianCalculated | partialFlag);
|
|
146
|
+
|
|
147
|
+
return { durationGood, durationBad, durationUnknown, percentBad, percentGood, statusCode };
|
|
148
|
+
}
|
package/source/common.ts
CHANGED
|
@@ -5,21 +5,14 @@ import { SessionContext, UAVariable, ContinuationPointManager, ContinuationPoint
|
|
|
5
5
|
import { NodeClass } from "node-opcua-data-model";
|
|
6
6
|
import { DataValue } from "node-opcua-data-value";
|
|
7
7
|
import { HistoryData, HistoryReadResult, ReadRawModifiedDetails } from "node-opcua-service-history";
|
|
8
|
-
import { StatusCode } from "node-opcua-status-code";
|
|
8
|
+
import { StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
9
9
|
import { coerceNodeId } from "node-opcua-nodeid";
|
|
10
10
|
|
|
11
11
|
import { getAggregateConfiguration } from "./aggregates";
|
|
12
|
-
import { getInterval, Interval, AggregateConfigurationOptionsEx } from "./interval";
|
|
12
|
+
import { getInterval, Interval, AggregateConfigurationOptionsEx, isBad, isGood, isUncertain } from "./interval";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @internal
|
|
16
|
-
* @param node
|
|
17
|
-
* @param processingInterval
|
|
18
|
-
* @param startDate
|
|
19
|
-
* @param endDate
|
|
20
|
-
* @param dataValues
|
|
21
|
-
* @param lambda
|
|
22
|
-
* @param callback
|
|
23
16
|
*/
|
|
24
17
|
function processAggregateData(
|
|
25
18
|
node: UAVariable,
|
|
@@ -52,6 +45,7 @@ function processAggregateData(
|
|
|
52
45
|
throw Error("invalid DataValue");
|
|
53
46
|
}
|
|
54
47
|
results.push(dataValue);
|
|
48
|
+
// console.log(" => ", dataValue.sourceTimestamp.toISOString(), dataValue.statusCode.toString(), dataValue.value.value);
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
setImmediate(() => {
|
package/source/count.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { UAVariable } from "node-opcua-address-space";
|
|
2
|
+
import { DataValue } from "node-opcua-data-value";
|
|
3
|
+
import { extraStatusCodeBits, StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
4
|
+
import { DataType } from "node-opcua-variant";
|
|
5
|
+
|
|
6
|
+
import { getAggregateData } from "./common";
|
|
7
|
+
import { Interval, AggregateConfigurationOptions, isBad, isUncertain, isGoodish } from "./interval";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Count Aggregate retrieves a count of all the raw values within an interval.
|
|
11
|
+
* If one or more raw values are non-Good, they are not included in the count, and the Aggregate
|
|
12
|
+
* StatusCode is determined using the StatusCode Calculation for non-time based Aggregates.
|
|
13
|
+
* If no Good data exists for an interval, the count is zero.
|
|
14
|
+
*/
|
|
15
|
+
function calculateCountValue(interval: Interval, options: AggregateConfigurationOptions): DataValue {
|
|
16
|
+
const indexStart = interval.index;
|
|
17
|
+
let statusCode: StatusCode = StatusCodes.Good;
|
|
18
|
+
let isPartial = interval.isPartial;
|
|
19
|
+
|
|
20
|
+
let nbBad = 0;
|
|
21
|
+
let nbGood = 0;
|
|
22
|
+
let nbUncertain = 0;
|
|
23
|
+
let badDuration = 0;
|
|
24
|
+
let uncertainDuration = 0;
|
|
25
|
+
let goodDuration = 0;
|
|
26
|
+
for (let i = indexStart; i < indexStart + interval.count; i++) {
|
|
27
|
+
const dataValue = interval.dataValues[i];
|
|
28
|
+
if (dataValue.statusCode === StatusCodes.BadNoData) {
|
|
29
|
+
isPartial = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const regionDuration = interval.regionDuration(i);
|
|
33
|
+
if (isBad(dataValue.statusCode)) {
|
|
34
|
+
nbBad++;
|
|
35
|
+
badDuration += regionDuration;
|
|
36
|
+
} else if (isUncertain(dataValue.statusCode)) {
|
|
37
|
+
nbUncertain++;
|
|
38
|
+
uncertainDuration += regionDuration;
|
|
39
|
+
} else if (isGoodish(dataValue.statusCode)) {
|
|
40
|
+
nbGood++;
|
|
41
|
+
goodDuration += regionDuration;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const partialFlag = isPartial ? extraStatusCodeBits.HistorianPartial : 0;
|
|
46
|
+
|
|
47
|
+
// console.log(" ", goodDuration, uncertainDuration, badDuration);
|
|
48
|
+
|
|
49
|
+
if (nbBad > 0) {
|
|
50
|
+
const duration = interval.duration();
|
|
51
|
+
if(options.treatUncertainAsBad) {
|
|
52
|
+
badDuration += uncertainDuration;
|
|
53
|
+
}
|
|
54
|
+
const actualPercentDataBad = (badDuration / duration) * 100.0;
|
|
55
|
+
const percentDataBad = options.percentDataBad === undefined ? 100 : options.percentDataBad;
|
|
56
|
+
if (actualPercentDataBad >= percentDataBad) {
|
|
57
|
+
return new DataValue({
|
|
58
|
+
sourceTimestamp: interval.startTime,
|
|
59
|
+
statusCode: StatusCodes.Bad
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (nbUncertain > 0 || nbBad > 0) {
|
|
64
|
+
statusCode = StatusCode.makeStatusCode(
|
|
65
|
+
StatusCodes.UncertainDataSubNormal,
|
|
66
|
+
extraStatusCodeBits.HistorianCalculated | partialFlag
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
statusCode = StatusCode.makeStatusCode(StatusCodes.Good, extraStatusCodeBits.HistorianCalculated | partialFlag);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (nbUncertain === 0 && nbGood === 0) {
|
|
73
|
+
statusCode = StatusCodes.BadNoData;
|
|
74
|
+
return new DataValue({
|
|
75
|
+
sourceTimestamp: interval.startTime,
|
|
76
|
+
statusCode: statusCode as StatusCode
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return new DataValue({
|
|
81
|
+
sourceTimestamp: interval.startTime,
|
|
82
|
+
statusCode: statusCode as StatusCode,
|
|
83
|
+
value: {
|
|
84
|
+
dataType: DataType.UInt32,
|
|
85
|
+
value: nbGood
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getCountData(
|
|
91
|
+
node: UAVariable,
|
|
92
|
+
processingInterval: number,
|
|
93
|
+
startDate: Date,
|
|
94
|
+
endDate: Date,
|
|
95
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
96
|
+
): void {
|
|
97
|
+
getAggregateData(node, processingInterval, startDate, endDate, calculateCountValue, callback);
|
|
98
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UAVariable } from "node-opcua-address-space";
|
|
2
|
+
import { DataValue } from "node-opcua-data-value";
|
|
3
|
+
import { DataType } from "node-opcua-variant";
|
|
4
|
+
import { StatusCodes } from "node-opcua-status-code";
|
|
5
|
+
|
|
6
|
+
import { getAggregateData } from "./common";
|
|
7
|
+
import { Interval, AggregateConfigurationOptions, isGoodish } from "./interval";
|
|
8
|
+
import { calculateBadAndGood } from "./calculate_bad_good";
|
|
9
|
+
|
|
10
|
+
function calculateDurationBad(interval: Interval, options: AggregateConfigurationOptions): DataValue {
|
|
11
|
+
const { durationBad, durationUnknown, statusCode } = calculateBadAndGood(interval, options);
|
|
12
|
+
if (durationUnknown > 0 && durationBad === 0) {
|
|
13
|
+
return new DataValue({
|
|
14
|
+
sourceTimestamp: interval.startTime,
|
|
15
|
+
statusCode: StatusCodes.Bad
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const value = durationBad;
|
|
19
|
+
if (isGoodish(statusCode)) {
|
|
20
|
+
return new DataValue({
|
|
21
|
+
sourceTimestamp: interval.startTime,
|
|
22
|
+
statusCode,
|
|
23
|
+
value: { dataType: DataType.Double, value }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return new DataValue({ sourceTimestamp: interval.startTime, statusCode, value: { dataType: DataType.Null } });
|
|
27
|
+
}
|
|
28
|
+
/**Retrieve the percentage of data (0 to 100) in the interval which has Bad StatusCode. */
|
|
29
|
+
export function getDurationBadData(
|
|
30
|
+
node: UAVariable,
|
|
31
|
+
processingInterval: number,
|
|
32
|
+
startDate: Date,
|
|
33
|
+
endDate: Date,
|
|
34
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
35
|
+
): void {
|
|
36
|
+
getAggregateData(node, processingInterval, startDate, endDate, calculateDurationBad, callback);
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UAVariable } from "node-opcua-address-space";
|
|
2
|
+
import { DataValue } from "node-opcua-data-value";
|
|
3
|
+
import { DataType } from "node-opcua-variant";
|
|
4
|
+
import { StatusCodes } from "node-opcua-status-code";
|
|
5
|
+
|
|
6
|
+
import { getAggregateData } from "./common";
|
|
7
|
+
import { Interval, AggregateConfigurationOptions, isGoodish } from "./interval";
|
|
8
|
+
import { calculateBadAndGood } from "./calculate_bad_good";
|
|
9
|
+
|
|
10
|
+
function calculateDurationGood(interval: Interval, options: AggregateConfigurationOptions): DataValue {
|
|
11
|
+
const { durationGood, durationUnknown, statusCode } = calculateBadAndGood(interval, options);
|
|
12
|
+
if (durationUnknown>0 && durationGood ===0) {
|
|
13
|
+
return new DataValue({
|
|
14
|
+
sourceTimestamp: interval.startTime,
|
|
15
|
+
statusCode: StatusCodes.Bad
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const value = durationGood;
|
|
19
|
+
if (isGoodish(statusCode)) {
|
|
20
|
+
return new DataValue({
|
|
21
|
+
sourceTimestamp: interval.startTime,
|
|
22
|
+
statusCode,
|
|
23
|
+
value: { dataType: DataType.Double, value }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return new DataValue({ sourceTimestamp: interval.startTime, statusCode, value: { dataType: DataType.Null } });
|
|
27
|
+
}
|
|
28
|
+
/**Retrieve the percentage of data (0 to 100) in the interval which has Bad StatusCode. */
|
|
29
|
+
export function getDurationGoodData(
|
|
30
|
+
node: UAVariable,
|
|
31
|
+
processingInterval: number,
|
|
32
|
+
startDate: Date,
|
|
33
|
+
endDate: Date,
|
|
34
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
35
|
+
): void {
|
|
36
|
+
getAggregateData(node, processingInterval, startDate, endDate, calculateDurationGood, callback);
|
|
37
|
+
}
|
package/source/index.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opca-aggregates
|
|
3
3
|
*/
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
addAggregateSupport,
|
|
6
|
+
installAggregateConfigurationOptions,
|
|
7
|
+
getAggregateConfiguration,
|
|
8
|
+
addAggregateStandardFunctionSupport,
|
|
9
|
+
addAggregateFunctionSupport
|
|
10
|
+
} from "./aggregates";
|
|
5
11
|
export * from "./interpolate";
|
|
6
12
|
export * from "./minmax";
|
|
7
13
|
export * from "./interval";
|