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,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opca-aggregates
|
|
3
|
+
*/
|
|
4
|
+
import { DataValue } from "node-opcua-data-value";
|
|
5
|
+
import { StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
6
|
+
import { AggregateConfigurationOptions } from "node-opcua-types";
|
|
7
|
+
export { AggregateConfigurationOptions } from "node-opcua-types";
|
|
8
|
+
|
|
9
|
+
export interface AggregateConfigurationOptionsEx extends AggregateConfigurationOptions {
|
|
10
|
+
stepped?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export function isGoodish(statusCode: StatusCode): boolean {
|
|
13
|
+
return statusCode.value < 0x40000000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isBad(statusCode: StatusCode): boolean {
|
|
17
|
+
return statusCode.value >= 0x80000000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isGood(statusCode: StatusCode): boolean {
|
|
21
|
+
return statusCode.value === 0x0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IntervalOptions {
|
|
25
|
+
startTime: Date;
|
|
26
|
+
dataValues: DataValue[];
|
|
27
|
+
index: number;
|
|
28
|
+
count: number;
|
|
29
|
+
isPartial: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DataValueWithIndex {
|
|
33
|
+
index: number;
|
|
34
|
+
dataValue: DataValue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function _findGoodDataValueBefore(
|
|
38
|
+
dataValues: DataValue[],
|
|
39
|
+
index: number,
|
|
40
|
+
bTreatUncertainAsBad: boolean
|
|
41
|
+
): DataValueWithIndex {
|
|
42
|
+
index--;
|
|
43
|
+
while (index >= 0) {
|
|
44
|
+
const dataValue1 = dataValues[index];
|
|
45
|
+
if (!bTreatUncertainAsBad && !isBad(dataValue1.statusCode)) {
|
|
46
|
+
return { index, dataValue: dataValue1 };
|
|
47
|
+
}
|
|
48
|
+
if (bTreatUncertainAsBad && isGood(dataValue1.statusCode)) {
|
|
49
|
+
return { index, dataValue: dataValue1 };
|
|
50
|
+
}
|
|
51
|
+
index -= 1;
|
|
52
|
+
}
|
|
53
|
+
// not found
|
|
54
|
+
return {
|
|
55
|
+
dataValue: new DataValue({ statusCode: StatusCodes.BadNoData }),
|
|
56
|
+
index: -1
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function _findGoodDataValueAfter(dataValues: DataValue[], index: number, bTreatUncertainAsBad: boolean): DataValueWithIndex {
|
|
61
|
+
while (index < dataValues.length) {
|
|
62
|
+
const dataValue1 = dataValues[index];
|
|
63
|
+
if (!bTreatUncertainAsBad && !isBad(dataValue1.statusCode)) {
|
|
64
|
+
return {
|
|
65
|
+
dataValue: dataValue1,
|
|
66
|
+
index
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (bTreatUncertainAsBad && isGood(dataValue1.statusCode)) {
|
|
70
|
+
return {
|
|
71
|
+
dataValue: dataValue1,
|
|
72
|
+
index
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
index += 1;
|
|
76
|
+
}
|
|
77
|
+
// not found
|
|
78
|
+
return {
|
|
79
|
+
dataValue: new DataValue({ statusCode: StatusCodes.BadNoData }),
|
|
80
|
+
index: -1
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function adjustProcessingOptions(options: AggregateConfigurationOptionsEx | null): AggregateConfigurationOptionsEx {
|
|
85
|
+
options = options || {};
|
|
86
|
+
options.treatUncertainAsBad = options.treatUncertainAsBad || false;
|
|
87
|
+
options.useSlopedExtrapolation = options.useSlopedExtrapolation || false;
|
|
88
|
+
options.stepped = options.stepped! || false;
|
|
89
|
+
options.percentDataBad = parseInt(options.percentDataBad as any, 10);
|
|
90
|
+
options.percentDataGood = parseInt(options.percentDataGood as any, 10);
|
|
91
|
+
return options;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class Interval {
|
|
95
|
+
public startTime: Date;
|
|
96
|
+
public dataValues: DataValue[];
|
|
97
|
+
public index: number;
|
|
98
|
+
public count: number;
|
|
99
|
+
public isPartial: boolean;
|
|
100
|
+
|
|
101
|
+
// startTime
|
|
102
|
+
// dataValues
|
|
103
|
+
// index: index of first dataValue inside the interval
|
|
104
|
+
// count: number of dataValue inside the interval
|
|
105
|
+
constructor(options: IntervalOptions) {
|
|
106
|
+
this.startTime = options.startTime;
|
|
107
|
+
this.dataValues = options.dataValues;
|
|
108
|
+
this.index = options.index;
|
|
109
|
+
this.count = options.count;
|
|
110
|
+
this.isPartial = options.isPartial;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public getPercentBad(): number {
|
|
114
|
+
return 100;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* returns true if a raw data exists at start
|
|
119
|
+
*/
|
|
120
|
+
public hasRawDataAsStart() {
|
|
121
|
+
const index = this.index;
|
|
122
|
+
if (index < 0) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const dataValue1 = this.dataValues[index];
|
|
126
|
+
return this.startTime.getTime() === dataValue1!.sourceTimestamp!.getTime();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Find the first good or uncertain dataValue
|
|
131
|
+
* just preceding this interval
|
|
132
|
+
* @returns {*}
|
|
133
|
+
*/
|
|
134
|
+
public beforeStartDataValue(bTreatUncertainAsBad: boolean) {
|
|
135
|
+
return _findGoodDataValueBefore(this.dataValues, this.index, bTreatUncertainAsBad);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public nextStartDataValue(bTreatUncertainAsBad: boolean) {
|
|
139
|
+
return _findGoodDataValueAfter(this.dataValues, this.index, bTreatUncertainAsBad);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public toString() {
|
|
143
|
+
let str = "";
|
|
144
|
+
str += "startTime " + this.startTime.toUTCString() + "\n";
|
|
145
|
+
str += "start " + this.index + " ";
|
|
146
|
+
str += "count " + this.count + " ";
|
|
147
|
+
str += "isPartial " + this.isPartial + "\n";
|
|
148
|
+
if (this.index >= 0) {
|
|
149
|
+
for (let i = this.index; i < this.index + this.count; i++) {
|
|
150
|
+
const dataValue = this.dataValues[i];
|
|
151
|
+
str += " " + dataValue.sourceTimestamp!.toUTCString() + dataValue.statusCode.toString();
|
|
152
|
+
str += dataValue.value ? dataValue.value.toString() : "";
|
|
153
|
+
str += "\n";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return str;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getInterval(startTime: Date, duration: number, indexHint: number, dataValues: DataValue[]): Interval {
|
|
161
|
+
let count = 0;
|
|
162
|
+
let index = -1;
|
|
163
|
+
for (let i = indexHint; i < dataValues.length; i++) {
|
|
164
|
+
if (dataValues[i].sourceTimestamp!.getTime() < startTime.getTime()) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
index = i;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (index >= 0) {
|
|
172
|
+
for (let i = index; i < dataValues.length; i++) {
|
|
173
|
+
if (dataValues[i].sourceTimestamp!.getTime() >= startTime.getTime() + duration) {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
count++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// check if interval is complete or partial (end or start)
|
|
181
|
+
let isPartial = false;
|
|
182
|
+
if (
|
|
183
|
+
index + count >= dataValues.length &&
|
|
184
|
+
dataValues[dataValues.length - 1].sourceTimestamp!.getTime() < startTime.getTime() + duration
|
|
185
|
+
) {
|
|
186
|
+
isPartial = true;
|
|
187
|
+
}
|
|
188
|
+
if (index <= 0 && dataValues[0].sourceTimestamp!.getTime() > startTime.getTime()) {
|
|
189
|
+
isPartial = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return new Interval({
|
|
193
|
+
count,
|
|
194
|
+
dataValues,
|
|
195
|
+
index,
|
|
196
|
+
isPartial,
|
|
197
|
+
startTime
|
|
198
|
+
});
|
|
199
|
+
}
|
package/source/minmax.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opca-aggregates
|
|
3
|
+
*/
|
|
4
|
+
// excert from OPC Unified Architecture, Part 13 21 Release 1.04
|
|
5
|
+
// 5.4.3.10 Minimum
|
|
6
|
+
// The Minimum Aggregate defined in Table 21 retrieves the minimum Good raw value within the
|
|
7
|
+
// interval, and returns that value with the timestamp at the start of the interval. Note that if the
|
|
8
|
+
// same minimum exists at more than one timestamp the MultipleValues bit is set.
|
|
9
|
+
//
|
|
10
|
+
// Unless otherwise indicated, StatusCodes are Good, Calculated. If the minimum value is on
|
|
11
|
+
// the start time the status code will be Good, Raw. If only Bad quality values are available then
|
|
12
|
+
// the status is returned as Bad_NoData.
|
|
13
|
+
//
|
|
14
|
+
// The timestamp of the Aggregate will always be the start of the interval for every
|
|
15
|
+
// ProcessingInterval.
|
|
16
|
+
//
|
|
17
|
+
// Table 21 – Minimum Aggregate summary
|
|
18
|
+
//
|
|
19
|
+
// Minimum Aggregate Characteristics
|
|
20
|
+
//
|
|
21
|
+
// Type Calculated
|
|
22
|
+
// Data Type Same as Source
|
|
23
|
+
// Use Bounds None
|
|
24
|
+
// Timestamp StartTime
|
|
25
|
+
//
|
|
26
|
+
// Status Code Calculations
|
|
27
|
+
//
|
|
28
|
+
// Calculation Method Custom
|
|
29
|
+
// If no Bad values then the Status is Good. If Bad values exist then
|
|
30
|
+
// the Status is Uncertain_SubNormal. If an Uncertain value is less
|
|
31
|
+
// than the minimum Good value the Status is Uncertain_SubNormal.
|
|
32
|
+
//
|
|
33
|
+
// Partial Set Sometimes
|
|
34
|
+
// If an interval is not a complete interval
|
|
35
|
+
//
|
|
36
|
+
// Calculated Set Sometimes
|
|
37
|
+
// If the Minimum value is not on the StartTime of the interval or if the
|
|
38
|
+
// Status was set to Uncertain_SubNormal because of non-Good
|
|
39
|
+
// values in the interval
|
|
40
|
+
//
|
|
41
|
+
// Interpolated Not Set
|
|
42
|
+
// Raw Set Sometimes
|
|
43
|
+
// If Minimum value is on the StartTime of the interval
|
|
44
|
+
//
|
|
45
|
+
// Multi Value Set Sometimes
|
|
46
|
+
// If multiple Good values exist with the Minimum value
|
|
47
|
+
//
|
|
48
|
+
// Status Code Common Special Cases
|
|
49
|
+
// Before Start of Data Bad_NoData
|
|
50
|
+
// After End of Data Bad_NoData
|
|
51
|
+
// No Start Bound Not Applicable
|
|
52
|
+
// No End Bound Not Applicable
|
|
53
|
+
// Bound Bad Not Applicable
|
|
54
|
+
// Bound Uncertain Not Applicable
|
|
55
|
+
import { UAVariable } from "node-opcua-address-space";
|
|
56
|
+
import { DataValue } from "node-opcua-data-value";
|
|
57
|
+
import { StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
58
|
+
import { Variant } from "node-opcua-variant";
|
|
59
|
+
|
|
60
|
+
import { getAggregateData } from "./common";
|
|
61
|
+
import { AggregateConfigurationOptions, Interval, isGood } from "./interval";
|
|
62
|
+
|
|
63
|
+
function calculateIntervalMinOrMaxValue(
|
|
64
|
+
interval: Interval,
|
|
65
|
+
options: AggregateConfigurationOptions,
|
|
66
|
+
predicate: (a: Variant, b: Variant) => "equal" | "select" | "reject"
|
|
67
|
+
): DataValue {
|
|
68
|
+
|
|
69
|
+
// console.log(interval.toString());
|
|
70
|
+
|
|
71
|
+
const indexStart = interval.index;
|
|
72
|
+
let selectedValue: Variant | null = null;
|
|
73
|
+
|
|
74
|
+
let counter = 0;
|
|
75
|
+
let statusCode: StatusCode;
|
|
76
|
+
let isPartial = interval.isPartial;
|
|
77
|
+
|
|
78
|
+
let isRaw = false;
|
|
79
|
+
let hasBad = false;
|
|
80
|
+
|
|
81
|
+
for (let i = indexStart; i < indexStart + interval.count; i++) {
|
|
82
|
+
const dataValue = interval.dataValues[i];
|
|
83
|
+
|
|
84
|
+
if (dataValue.statusCode === StatusCodes.BadNoData) {
|
|
85
|
+
isPartial = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!isGood(dataValue.statusCode)) {
|
|
90
|
+
hasBad = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!selectedValue) {
|
|
95
|
+
selectedValue = dataValue.value;
|
|
96
|
+
counter = 1;
|
|
97
|
+
if (i === indexStart && dataValue.sourceTimestamp!.getTime() === interval.startTime.getTime()) {
|
|
98
|
+
isRaw = true;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const compare = predicate(selectedValue, dataValue.value);
|
|
103
|
+
if (compare === "equal") {
|
|
104
|
+
counter = 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (compare === "select") {
|
|
108
|
+
selectedValue = dataValue.value;
|
|
109
|
+
counter = 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!selectedValue) {
|
|
114
|
+
return new DataValue({
|
|
115
|
+
sourceTimestamp: interval.startTime,
|
|
116
|
+
statusCode: StatusCodes.BadNoData
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (isRaw) {
|
|
120
|
+
if (hasBad) {
|
|
121
|
+
statusCode = StatusCodes.UncertainDataSubNormal;
|
|
122
|
+
} else {
|
|
123
|
+
statusCode = StatusCodes.Good;
|
|
124
|
+
}
|
|
125
|
+
} else if (hasBad) {
|
|
126
|
+
statusCode = StatusCode.makeStatusCode(StatusCodes.UncertainDataSubNormal, "HistorianCalculated");
|
|
127
|
+
} else {
|
|
128
|
+
statusCode = StatusCode.makeStatusCode(StatusCodes.Good, "HistorianCalculated");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (counter > 1) {
|
|
132
|
+
statusCode = StatusCode.makeStatusCode(statusCode, "HistorianMultiValue");
|
|
133
|
+
}
|
|
134
|
+
if (isPartial || interval.isPartial) {
|
|
135
|
+
statusCode = StatusCode.makeStatusCode(statusCode, "HistorianPartial");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return new DataValue({
|
|
139
|
+
sourceTimestamp: interval.startTime,
|
|
140
|
+
statusCode: statusCode as StatusCode,
|
|
141
|
+
value: selectedValue!
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function calculateIntervalMinValue(interval: Interval, options: AggregateConfigurationOptions): DataValue {
|
|
146
|
+
return calculateIntervalMinOrMaxValue(interval, options,
|
|
147
|
+
(a: Variant, b: Variant) =>
|
|
148
|
+
a.value > b.value ? "select" : (a.value === b.value ? "equal" : "reject"));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function calculateIntervalMaxValue(interval: Interval, options: AggregateConfigurationOptions): DataValue {
|
|
152
|
+
return calculateIntervalMinOrMaxValue(interval, options,
|
|
153
|
+
(a: Variant, b: Variant) =>
|
|
154
|
+
a.value < b.value ? "select" : (a.value === b.value ? "equal" : "reject"));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// From OPC Unified Architecture, Part 13 26 Release 1.04
|
|
158
|
+
// 5.4.3.11 Maximum
|
|
159
|
+
// The Maximum Aggregate defined in Table 22 retrieves the maximum Good raw value within
|
|
160
|
+
// the interval, and returns that value with the timestamp at the start of the interval. Note that if
|
|
161
|
+
// the same maximum exists at more than one timestamp the MultipleValues bit is set.
|
|
162
|
+
// Unless otherwise indicated, StatusCodes are Good, Calculated. If the minimum value is on
|
|
163
|
+
// the interval start time the status code will be Good, Raw. If only Bad quality values are
|
|
164
|
+
// available then the status is returned as Bad_NoData.
|
|
165
|
+
// The timestamp of the Aggregate will always be the start of the interval for every
|
|
166
|
+
//
|
|
167
|
+
// ProcessingInterval.
|
|
168
|
+
//
|
|
169
|
+
// Table 22 – Maximum Aggregate summary
|
|
170
|
+
// Maximum Aggregate Characteristics
|
|
171
|
+
//
|
|
172
|
+
// Type Calculated
|
|
173
|
+
// Data Type Same as Source
|
|
174
|
+
// Use Bounds None
|
|
175
|
+
// Timestamp StartTime
|
|
176
|
+
//
|
|
177
|
+
// Status Code Calculations
|
|
178
|
+
// Calculation Method Custom
|
|
179
|
+
// If no Bad values then the Status is Good. If Bad values exist then
|
|
180
|
+
// the Status is Uncertain_SubNormal. If an Uncertain value is greater
|
|
181
|
+
// than the maximum Good value the Status is Uncertain_SubNormal
|
|
182
|
+
//
|
|
183
|
+
// Partial Set Sometimes
|
|
184
|
+
// If an interval is not a complete interval
|
|
185
|
+
//
|
|
186
|
+
// Calculated Set Sometimes
|
|
187
|
+
// If the Maximum value is not on the startTime of the interval or if the
|
|
188
|
+
// Status was set to Uncertain_SubNormal because of non-Good
|
|
189
|
+
// values in the interval
|
|
190
|
+
//
|
|
191
|
+
// Interpolated Not Set
|
|
192
|
+
//
|
|
193
|
+
// Raw Set Sometimes
|
|
194
|
+
// If Maximum value is on the startTime of the interval
|
|
195
|
+
// Multi Value Set Sometimes
|
|
196
|
+
// If multiple Good values exist with the Maximum value
|
|
197
|
+
//
|
|
198
|
+
// Status Code Common Special Cases
|
|
199
|
+
// Before Start of Data Bad_NoData
|
|
200
|
+
// After End of Data Bad_NoData
|
|
201
|
+
// No Start Bound Not Applicable
|
|
202
|
+
// No End Bound Not Applicable
|
|
203
|
+
// Bound Bad Not Applicable
|
|
204
|
+
// Bound Uncertain Not Applicable
|
|
205
|
+
/**
|
|
206
|
+
*
|
|
207
|
+
* @param node
|
|
208
|
+
* @param processingInterval
|
|
209
|
+
* @param startDate
|
|
210
|
+
* @param endDate
|
|
211
|
+
* @param callback
|
|
212
|
+
*/
|
|
213
|
+
export function getMinData(
|
|
214
|
+
node: UAVariable,
|
|
215
|
+
processingInterval: number,
|
|
216
|
+
startDate: Date,
|
|
217
|
+
endDate: Date,
|
|
218
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
219
|
+
) {
|
|
220
|
+
return getAggregateData(node, processingInterval, startDate, endDate, calculateIntervalMinValue, callback);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function getMaxData(
|
|
224
|
+
node: UAVariable,
|
|
225
|
+
processingInterval: number,
|
|
226
|
+
startDate: Date,
|
|
227
|
+
endDate: Date,
|
|
228
|
+
callback: (err: Error | null, dataValues?: DataValue[]) => void
|
|
229
|
+
) {
|
|
230
|
+
return getAggregateData(node, processingInterval, startDate, endDate, calculateIntervalMaxValue, callback);
|
|
231
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as async from "async";
|
|
2
|
+
import { AggregateFunction } from "node-opcua-constants";
|
|
3
|
+
import { ISessionContext, ContinuationPoint, UAVariable } from "node-opcua-address-space";
|
|
4
|
+
import { NumericRange } from "node-opcua-numeric-range";
|
|
5
|
+
import { QualifiedNameLike } from "node-opcua-data-model";
|
|
6
|
+
import { CallbackT, StatusCodes } from "node-opcua-status-code";
|
|
7
|
+
import { DataValue } from "node-opcua-data-value";
|
|
8
|
+
import { ObjectIds } from "node-opcua-constants";
|
|
9
|
+
import { NodeId } from "node-opcua-nodeid";
|
|
10
|
+
import { getMinData, getMaxData } from "./minmax";
|
|
11
|
+
import {
|
|
12
|
+
HistoryData,
|
|
13
|
+
HistoryReadResult,
|
|
14
|
+
ReadAtTimeDetails,
|
|
15
|
+
ReadEventDetails,
|
|
16
|
+
ReadProcessedDetails,
|
|
17
|
+
ReadRawModifiedDetails
|
|
18
|
+
} from "node-opcua-service-history";
|
|
19
|
+
|
|
20
|
+
import { getInterpolatedData } from "./interpolate";
|
|
21
|
+
import { getAverageData } from "./average";
|
|
22
|
+
|
|
23
|
+
export function readProcessedDetails(
|
|
24
|
+
variable: UAVariable,
|
|
25
|
+
context: ISessionContext,
|
|
26
|
+
historyReadDetails: ReadProcessedDetails,
|
|
27
|
+
indexRange: NumericRange | null,
|
|
28
|
+
dataEncoding: QualifiedNameLike | null,
|
|
29
|
+
continuationPoint: ContinuationPoint | null,
|
|
30
|
+
callback: CallbackT<HistoryReadResult>
|
|
31
|
+
) {
|
|
32
|
+
// OPC Unified Architecture, Part 11 27 Release 1.03
|
|
33
|
+
//
|
|
34
|
+
// This structure is used to compute Aggregate values, qualities, and timestamps from data in
|
|
35
|
+
// the history database for the specified time domain for one or more HistoricalDataNodes. The
|
|
36
|
+
// time domain is divided into intervals of duration ProcessingInterval. The specified Aggregate
|
|
37
|
+
// Type is calculated for each interval beginning with startTime by using the data within the next
|
|
38
|
+
// ProcessingInterval.
|
|
39
|
+
// For example, this function can provide hourly statistics such as Maximum, Minimum , and
|
|
40
|
+
// Average for each item during the specified time domain when ProcessingInterval is 1 hour.
|
|
41
|
+
// The domain of the request is defined by startTime, endTime, and ProcessingInterval. All three
|
|
42
|
+
// shall be specified. If endTime is less than startTime then the data shall be returned in reverse
|
|
43
|
+
// order with the later data coming first. If startTime and endTime are the same then the Server
|
|
44
|
+
// shall return Bad_InvalidArgument as there is no meaningful way to interpret such a case. If
|
|
45
|
+
// the ProcessingInterval is specified as 0 then Aggregates shall be calculated using one interval
|
|
46
|
+
// starting at startTime and ending at endTime.
|
|
47
|
+
// The aggregateType[] parameter allows a Client to request multiple Aggregate calculations per
|
|
48
|
+
// requested NodeId. If multiple Aggregates are requested then a corresponding number of
|
|
49
|
+
// entries are required in the NodesToRead array.
|
|
50
|
+
// For example, to request Min Aggregate for NodeId FIC101, FIC102, and both Min and Max
|
|
51
|
+
// Aggregates for NodeId FIC103 would require NodeId FIC103 to appear twice in the
|
|
52
|
+
// NodesToRead array request parameter.
|
|
53
|
+
// aggregateType[] NodesToRead[]
|
|
54
|
+
// Min FIC101
|
|
55
|
+
// Min FIC102
|
|
56
|
+
// Min FIC103
|
|
57
|
+
// Max FIC103
|
|
58
|
+
// If the array of Aggregates does not match the array of NodesToRead then the Server shall
|
|
59
|
+
// return a StatusCode of Bad_AggregateListMismatch.
|
|
60
|
+
// The aggregateConfiguration parameter allows a Client to override the Aggregate configuration
|
|
61
|
+
// settings supplied by the AggregateConfiguration Object on a per call basis. See Part 13 for
|
|
62
|
+
// more information on Aggregate configurations. If the Server does not support the ability to
|
|
63
|
+
// override the Aggregate configuration settings then it shall return a StatusCode of Bad_
|
|
64
|
+
// AggregateConfigurationRejected. If the Aggregate is not valid for the Node then the
|
|
65
|
+
// StatusCode shall be Bad_AggregateNotSupported.
|
|
66
|
+
// The values used in computing the Aggregate for each interval shall include any value that
|
|
67
|
+
// falls exactly on the timestamp at the beginning of the interval, but shall not include any value
|
|
68
|
+
// that falls directly on the timestamp ending the interval. Thus, each value shall be included
|
|
69
|
+
// only once in the calculation. If the time domain is in reverse order then we consider the later
|
|
70
|
+
// timestamp to be the one beginning the sub interval, and the earlier timestamp to be the one
|
|
71
|
+
// ending it. Note that this means that simply swapping the start and end times will not result in
|
|
72
|
+
// getting the same values back in reverse order as the intervals being requested in the two
|
|
73
|
+
// cases are not the same.
|
|
74
|
+
// If an Aggregate is taking a long time to calculate then the Server can return partial results
|
|
75
|
+
// with a continuation point. This might be done if the calculation is going to take more time th an
|
|
76
|
+
// the Client timeout hint. In some cases it may take longer than the Client timeout hint to
|
|
77
|
+
// calculate even one Aggregate result. Then the Server may return zero results with a
|
|
78
|
+
// continuation point that allows the Server to resume the calculation on the next Client read
|
|
79
|
+
// call.
|
|
80
|
+
const startTime = historyReadDetails.startTime;
|
|
81
|
+
const endTime = historyReadDetails.endTime;
|
|
82
|
+
if (!startTime || !endTime) {
|
|
83
|
+
return callback(null, new HistoryReadResult({ statusCode: StatusCodes.BadInvalidArgument }));
|
|
84
|
+
}
|
|
85
|
+
if (startTime.getTime() === endTime.getTime()) {
|
|
86
|
+
// Start = End Int = Anything No intervals. Returns a Bad_InvalidArgument StatusCode,
|
|
87
|
+
// regardless of whether there is data at the specified time or not
|
|
88
|
+
return callback(null, new HistoryReadResult({ statusCode: StatusCodes.BadInvalidArgument }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const aggregateTypes: NodeId[] = historyReadDetails.aggregateType || [];
|
|
92
|
+
|
|
93
|
+
// If the ProcessingInterval is specified as 0 then Aggregates shall be calculated using one interval
|
|
94
|
+
// starting at startTime and ending at endTime.
|
|
95
|
+
const processingInterval = historyReadDetails.processingInterval || endTime.getTime() - startTime.getTime();
|
|
96
|
+
|
|
97
|
+
// tslint:disable-next-line: prefer-for-of
|
|
98
|
+
|
|
99
|
+
function applyAggregate(aggregateType: NodeId, callback2: (err: Error | null, result: HistoryReadResult) => void) {
|
|
100
|
+
function buildResult(err: Error | null, dataValues?: DataValue[]) {
|
|
101
|
+
if (err) {
|
|
102
|
+
return callback2(null, new HistoryReadResult({ statusCode: StatusCodes.BadInternalError }));
|
|
103
|
+
}
|
|
104
|
+
const result = new HistoryReadResult({
|
|
105
|
+
historyData: new HistoryData({
|
|
106
|
+
dataValues
|
|
107
|
+
}),
|
|
108
|
+
statusCode: StatusCodes.Good
|
|
109
|
+
});
|
|
110
|
+
return callback2(null, result);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!startTime || !endTime) {
|
|
114
|
+
return buildResult(new Error("Invalid date time"));
|
|
115
|
+
}
|
|
116
|
+
switch (aggregateType.value) {
|
|
117
|
+
case AggregateFunction.Minimum:
|
|
118
|
+
getMinData(variable, processingInterval, startTime, endTime, buildResult);
|
|
119
|
+
break;
|
|
120
|
+
case AggregateFunction.Maximum:
|
|
121
|
+
getMaxData(variable, processingInterval, startTime, endTime, buildResult);
|
|
122
|
+
break;
|
|
123
|
+
case AggregateFunction.Interpolative:
|
|
124
|
+
getInterpolatedData(variable, processingInterval, startTime, endTime, buildResult);
|
|
125
|
+
break;
|
|
126
|
+
case AggregateFunction.Average:
|
|
127
|
+
getAverageData(variable, processingInterval, startTime, endTime, buildResult);
|
|
128
|
+
break;
|
|
129
|
+
case AggregateFunction.Count:
|
|
130
|
+
default:
|
|
131
|
+
// todo provide correct implementation
|
|
132
|
+
return callback2(null, new HistoryReadResult({ statusCode: StatusCodes.BadAggregateNotSupported }));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (historyReadDetails.aggregateType?.length !== 1) {
|
|
136
|
+
return callback(null, new HistoryReadResult({ statusCode: StatusCodes.BadInternalError }));
|
|
137
|
+
}
|
|
138
|
+
return applyAggregate(aggregateTypes[0], callback);
|
|
139
|
+
}
|