Skip to content

Commit

Permalink
aggregates: add AggregateFunctions in Varaible HA Configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
erossignon committed Sep 27, 2022
1 parent 2a53d1f commit 24e245d
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 56 deletions.
114 changes: 72 additions & 42 deletions packages/node-opcua-aggregates/source/aggregates.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
/**
* @module node-opcua-aggregates
*/
import { AggregateFunction, ObjectTypeIds } from "node-opcua-constants";
import { coerceNodeId, makeNodeId, NodeIdLike, sameNodeId } from "node-opcua-nodeid";
import { AggregateFunction, ObjectIds, ObjectTypeIds, ReferenceTypeIds } from "node-opcua-constants";
import { coerceNodeId, makeNodeId, NodeId, NodeIdLike, resolveNodeId, sameNodeId } from "node-opcua-nodeid";
import * as utils from "node-opcua-utils";
import { DataType } from "node-opcua-variant";
import {
AddressSpace,
BaseNode,
IAddressSpace,
UAHistoricalDataConfiguration,
UAHistoryServerCapabilities,
UAObject,
UAServerCapabilities,
UAVariable
} from "node-opcua-address-space";
import { AddressSpacePrivate } from "node-opcua-address-space/src/address_space_private";
import { BrowseDirection, coerceQualifiedName, NodeClass, NodeClassMask } from "node-opcua-data-model";
import { assert } from "node-opcua-assert";

import { AggregateConfigurationOptionsEx } from "./interval";
import { readProcessedDetails } from "./read_processed_details";
import { NodeClass } from "node-opcua-data-model";

// import { HistoryServerCapabilities } from "node-opcua-server";

Expand Down Expand Up @@ -123,44 +125,6 @@ function setHistoricalServerCapabilities(historyServerCapabilities: any, default
// xx setBoolean("InsertAnnotationsCapability");
}

export type AggregateFunctionName =
| "AnnotationCount"
| "Average"
| "Count"
| "Delta"
| "DeltaBounds"
| "DurationBad"
| "DurationGood"
| "DurationInStateNonZero"
| "DurationInStateZero"
| "EndBound"
| "Interpolative"
| "Maximum"
| "Maximum2"
| "MaximumActualTime"
| "MaximumActualTime2"
| "Minimum"
| "Minimum2"
| "MinimumActualTime"
| "MinimumActualTime2"
| "NumberOfTransitions"
| "PercentBad"
| "PercentGood"
| "Range"
| "Range2"
| "StandardDeviationPopulation"
| "StandardDeviationSample"
| "Start"
| "StartBound"
| "TimeAverage"
| "TimeAverage2"
| "Total"
| "Total2"
| "VariancePopulation"
| "VarianceSample"
| "WorstQuality"
| "WorstQuality2";

interface UAHistoryServerCapabilitiesWithH extends UAServerCapabilities {
historyServerCapabilities: UAHistoryServerCapabilities;
}
Expand Down Expand Up @@ -263,8 +227,48 @@ export function addAggregateSupport(addressSpace: AddressSpace, aggregatedFuncti
interface BaseNodeWithHistoricalDataConfiguration extends UAVariable {
$historicalDataConfiguration: UAHistoricalDataConfiguration;
}
export function installAggregateConfigurationOptions(node: UAVariable, options: AggregateConfigurationOptionsEx): void {

export function getAggregateFunctions(addressSpace: IAddressSpace): NodeId[] {
const aggregateFunctionTypeNodeId = resolveNodeId(ObjectTypeIds.AggregateFunctionType);
const aggregateFunctions = addressSpace.findNode(ObjectIds.Server_ServerCapabilities_AggregateFunctions) as UAObject;
if (!aggregateFunctions) {
return [];
}
const referenceDescripitions = aggregateFunctions.browseNode({
referenceTypeId: ReferenceTypeIds.HierarchicalReferences,
resultMask: 63,
nodeClassMask: NodeClassMask.Object,
browseDirection: BrowseDirection.Forward,
includeSubtypes: true
});
const aggregateFunctionsNodeIds = referenceDescripitions
.filter((a) => sameNodeId(a.typeDefinition, aggregateFunctionTypeNodeId))
.map((a) => a.nodeId);
return aggregateFunctionsNodeIds;
}

/**
* Install aggregateConfiguration on an historizing variable
*
* @param node the variable on which to add the aggregateConfiguration.
* @param options the default AggregateConfigurationOptions.
* @param aggregateFunctions the aggregatedFunctions, if not specified the aggregatedFunction of ServerCapabilities.AggregatedFunction will be used.
*/
export function installAggregateConfigurationOptions(
node: UAVariable,
options: AggregateConfigurationOptionsEx,
aggregateFunctions?: NodeIdLike[]
): void {
const nodePriv = node as BaseNodeWithHistoricalDataConfiguration;

// istanbul ignore next
if (!nodePriv.historizing) {
throw new Error(
"variable.historizing is not set\n make sure addressSpace.installHistoricalDataNode(variable) has been called"
);
}

const aggregateConfiguration = nodePriv.$historicalDataConfiguration.aggregateConfiguration;

const f = (a: number | boolean | undefined, defaultValue: number | boolean): number | boolean =>
Expand All @@ -285,6 +289,32 @@ export function installAggregateConfigurationOptions(node: UAVariable, options:
dataType: "Boolean",
value: f(options.stepped, false)
});
// https://reference.opcfoundation.org/v104/Core/docs/Part13/4.4/
// Exposing Supported Functions and Capabilities
if (!aggregateFunctions) {
aggregateFunctions = getAggregateFunctions(node.addressSpace);
}

let uaAggregateFunctions = nodePriv.$historicalDataConfiguration.aggregateFunctions;
if (!uaAggregateFunctions) {
const namespace = nodePriv.namespace;
uaAggregateFunctions = namespace.addObject({
browseName: coerceQualifiedName({ name: "AggregateFunctions", namespaceIndex: 0 }),
componentOf: nodePriv.$historicalDataConfiguration
});
uaAggregateFunctions = nodePriv.$historicalDataConfiguration.aggregateFunctions;
}
// verify that all aggregateFunctions are of type AggregateFunctionType
// ... to do

const referenceType = resolveNodeId(ReferenceTypeIds.Organizes);
for (const nodeId of aggregateFunctions) {
uaAggregateFunctions!.addReference({
nodeId,
referenceType,
isForward: true
});
}
}

export function getAggregateConfiguration(node: BaseNode): AggregateConfigurationOptionsEx {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export function createHistorian1(addressSpace: AddressSpace): UAVariable {
dataType: "Float"
});

const options: AggregateConfigurationOptionsEx & IHistoricalDataNodeOptions = {
historian: undefined,
addressSpace.installHistoricalDataNode(node);
const options: AggregateConfigurationOptionsEx = {
percentDataBad: 100,
percentDataGood: 100, // Therefore if all values are Good then the
// quality will be Good, or if all values are Bad then the quality will be Bad, but if there is
Expand All @@ -57,9 +57,6 @@ export function createHistorian1(addressSpace: AddressSpace): UAVariable {
treatUncertainAsBad: false, // Therefore Uncertain values are included in Aggregate calls.
useSlopedExtrapolation: false // Therefore SteppedExtrapolation is used at end boundary conditions.
};

addressSpace.installHistoricalDataNode(node, options);

installAggregateConfigurationOptions(node, options);

// 12:00:00 - BadNoData First archive entry, Point created
Expand Down Expand Up @@ -97,9 +94,17 @@ export function createHistorian2(addressSpace: AddressSpace): UAVariable {
dataType: "Double"
});

const options = {};

addressSpace.installHistoricalDataNode(node, options);
addressSpace.installHistoricalDataNode(node);
const options: AggregateConfigurationOptionsEx = {
treatUncertainAsBad: true, // Therefore Uncertain values are treated as Bad, and not included in the Aggregate call.
percentDataBad: 100,
percentDataGood: 100, // Therefore if all values are Good then the
// quality will be Good, or if all values are Bad then the quality will be Bad, but if there is
// some Good and some Bad then the quality will be Uncertain
stepped: false, // Therefore SlopedInterpolation is used between data points.
useSlopedExtrapolation: false // Therefore SteppedExtrapolation is used at end boundary conditions.
};
installAggregateConfigurationOptions(node, options);

// 12:00:00 - Bad_NoData First archive entry, Point created
addHistory(node, "12:00:00", 10, StatusCodes.BadNoData);
Expand Down Expand Up @@ -195,16 +200,16 @@ export function createHistorian3(addressSpace: AddressSpace): UAVariable {
dataType: "Double"
});

const options: AggregateConfigurationOptionsEx & IHistoricalDataNodeOptions = {
addressSpace.installHistoricalDataNode(node);

const options: AggregateConfigurationOptionsEx = {
percentDataBad: 50,
percentDataGood: 50,
stepped: true, // therefore SteppedInterpolation is used between data points
treatUncertainAsBad: true, // therefore Uncertain values are treated as Bad,
// and not included in the Aggregate call.
useSlopedExtrapolation: false // therefore SteppedExtrapolation is used at end boundary conditions.
};

addressSpace.installHistoricalDataNode(node, options);
installAggregateConfigurationOptions(node, options);

// 12:00:00 - Bad_NoData First archive entry, Point created
Expand Down
73 changes: 70 additions & 3 deletions packages/node-opcua-aggregates/test/test_aggregates.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable max-statements */
import * as should from "should";

import { AddressSpace, UAVariable } from "node-opcua-address-space";
import { AddressSpace, BaseNode, UAObject, UAVariable } from "node-opcua-address-space";
import { DataValue } from "node-opcua-data-value";
import { nodesets } from "node-opcua-nodesets";
import { generateAddressSpace } from "node-opcua-address-space/nodeJS";
import { DataType } from "node-opcua-variant";

import { getInterpolatedData } from "..";
import { AggregateFunction, getInterpolatedData, installAggregateConfigurationOptions } from "..";
import { addAggregateSupport, getAggregateConfiguration } from "..";
import { getMaxData, getMinData } from "..";
import { getAverageData } from "../source/average";
Expand All @@ -30,14 +30,81 @@ describe("Aggregates ", () => {
addressSpace = AddressSpace.create();
const namespaces: string[] = [nodesets.standard];
await generateAddressSpace(addressSpace, namespaces);
addressSpace.registerNamespace("MyNamespace");
});
after(async () => {
afterEach(async () => {
addressSpace.dispose();
});

it("should augment the addressSpace with aggregate function support", async () => {
addAggregateSupport(addressSpace);
});

function extractAggregateFunction(uaVariable: UAVariable) {
const haConfiguration = uaVariable.getChildByName("HA Configuration") as UAObject;
if (!haConfiguration) {
throw new Error("Cannot find HA Configuration");
}
haConfiguration.getComponentByName("AggregateConfiguration");
const aggregateFunctionsFolder = haConfiguration.getComponentByName("AggregateFunctions") as UAObject;
const functions = aggregateFunctionsFolder.findReferencesAsObject("Organizes");
return functions;
}
it("should add aggregate support to a variable - form 1", async () => {
addAggregateSupport(addressSpace);
const uaVariable = addressSpace.getOwnNamespace().addVariable({
browseName: "Temperature",
dataType: DataType.Double,
organizedBy: addressSpace.rootFolder.objects.server
});

addressSpace.installHistoricalDataNode(uaVariable);
installAggregateConfigurationOptions(uaVariable, {});

const functions = extractAggregateFunction(uaVariable);

const f = functions.map((a: BaseNode) => a.browseName.name).sort();
f.should.eql(["Average", "Interpolative", "Maximum", "Minimum"]);
});

it("should add aggregate support to a variable - form 2", async () => {
addAggregateSupport(addressSpace);
const uaVariable = addressSpace.getOwnNamespace().addVariable({
browseName: "Temperature",
dataType: DataType.Double,
organizedBy: addressSpace.rootFolder.objects.server
});

addressSpace.installHistoricalDataNode(uaVariable);
installAggregateConfigurationOptions(uaVariable, {}, []);

const functions = extractAggregateFunction(uaVariable);

const f = functions.map((a: BaseNode) => a.browseName.name).sort();
f.should.eql([]);
});

it("should add aggregate support to a variable - form 3", async () => {
addAggregateSupport(addressSpace);
const uaVariable = addressSpace.getOwnNamespace().addVariable({
browseName: "Temperature",
dataType: DataType.Double,
organizedBy: addressSpace.rootFolder.objects.server
});

addressSpace.installHistoricalDataNode(uaVariable);
installAggregateConfigurationOptions(uaVariable, {}, [
AggregateFunction.Average,
AggregateFunction.DurationGood,
AggregateFunction.PercentGood,
AggregateFunction.Count
]);

const functions = extractAggregateFunction(uaVariable);

const f = functions.map((a: BaseNode) => a.browseName.name).sort();
f.should.eql(["Average", "Count", "DurationGood", "PercentGood"]);
});
});

describe("Aggregates - Function ", () => {
Expand Down

0 comments on commit 24e245d

Please sign in to comment.