node-opcua-aggregates 2.77.0 → 2.79.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.
Files changed (43) hide show
  1. package/dist/aggregates.d.ts +19 -4
  2. package/dist/aggregates.js +95 -20
  3. package/dist/aggregates.js.map +1 -1
  4. package/dist/calculate_bad_good.d.ts +10 -0
  5. package/dist/calculate_bad_good.js +125 -0
  6. package/dist/calculate_bad_good.js.map +1 -0
  7. package/dist/common.js +1 -7
  8. package/dist/common.js.map +1 -1
  9. package/dist/count.d.ts +3 -0
  10. package/dist/count.js +87 -0
  11. package/dist/count.js.map +1 -0
  12. package/dist/duration_bad.d.ts +4 -0
  13. package/dist/duration_bad.js +33 -0
  14. package/dist/duration_bad.js.map +1 -0
  15. package/dist/duration_good.d.ts +4 -0
  16. package/dist/duration_good.js +33 -0
  17. package/dist/duration_good.js.map +1 -0
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/interval.d.ts +17 -1
  22. package/dist/interval.js +52 -5
  23. package/dist/interval.js.map +1 -1
  24. package/dist/percent_bad.d.ts +4 -0
  25. package/dist/percent_bad.js +26 -0
  26. package/dist/percent_bad.js.map +1 -0
  27. package/dist/percent_good.d.ts +7 -0
  28. package/dist/percent_good.js +50 -0
  29. package/dist/percent_good.js.map +1 -0
  30. package/dist/read_processed_details.js +44 -19
  31. package/dist/read_processed_details.js.map +1 -1
  32. package/package.json +11 -11
  33. package/source/aggregates.ts +118 -62
  34. package/source/calculate_bad_good.ts +148 -0
  35. package/source/common.ts +3 -9
  36. package/source/count.ts +98 -0
  37. package/source/duration_bad.ts +37 -0
  38. package/source/duration_good.ts +37 -0
  39. package/source/index.ts +7 -1
  40. package/source/interval.ts +53 -4
  41. package/source/percent_bad.ts +30 -0
  42. package/source/percent_good.ts +55 -0
  43. package/source/read_processed_details.ts +44 -23
@@ -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 functionNodeId = makeNodeId(functionName);
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 " + functionName + " in addressSpace");
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 addAggregateSupport(addressSpace: AddressSpace): void {
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
- addAggregateFunctionSupport(addressSpace, AggregateFunction.Interpolative);
236
- addAggregateFunctionSupport(addressSpace, AggregateFunction.Minimum);
237
- addAggregateFunctionSupport(addressSpace, AggregateFunction.Maximum);
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
- export function installAggregateConfigurationOptions(node: UAVariable, options: AggregateConfigurationOptionsEx): void {
245
- const nodePriv = node as any;
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
- aggregateConfiguration.percentDataBad.setValueFromSource({ dataType: "Byte", value: options.percentDataBad });
248
- aggregateConfiguration.percentDataGood.setValueFromSource({ dataType: "Byte", value: options.percentDataGood });
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 any;
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(() => {
@@ -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 { addAggregateSupport, installAggregateConfigurationOptions, getAggregateConfiguration } from "./aggregates";
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";