"use strict";
/**
* @module opcua.address_space
* @class AddressSpace
*/
var assert = require("node-opcua-assert");
var util = require("util");
var _ = require("underscore");
var historizing_service = require("node-opcua-service-history");
var HistoryReadRequest = historizing_service.HistoryReadRequest;
var HistoryReadDetails = historizing_service.HistoryReadDetails;
var HistoryReadResult = historizing_service.HistoryReadResult;
var HistoryData = historizing_service.HistoryData;
var AccessLevelFlag = require("node-opcua-data-model").AccessLevelFlag;
var makeAccessLevel = require("node-opcua-data-model").makeAccessLevel;
var DataType = require("node-opcua-variant").DataType;
var StatusCodes = require("node-opcua-status-code").StatusCodes;
var UAVariable = require("../ua_variable").UAVariable;
var SessionContext = require("../session_context").SessionContext;
exports.install = function (AddressSpace) {
function inInTimeRange(historyReadDetails, dataValue) {
if (historyReadDetails.startTime && dataValue.sourceTimestamp < historyReadDetails.startTime) {
return false;
}
if (historyReadDetails.endTime && dataValue.sourceTimestamp > historyReadDetails.endTime) {
return false;
}
return true;
}
function filter_dequeue(q, predicate) {
var r = [];
var c = q.head.next;
while (c.data) {
if (predicate(c.data)) {
r.push(c.data);
}
c = c.next;
}
return r;
}
function _get_startOfOfflineArchive(node) {
return node.$historicalDataConfiguration.startOfArchive.readValue();
}
function _get_startOfArchive(node) {
return node.$historicalDataConfiguration.startOfArchive.readValue();
}
function _update_startOfOnlineArchive(newDate) {
var node = this;
// The StartOfArchive Variable specifies the date before which there is no data in the archive either online or offline.
// The StartOfOnlineArchive Variable specifies the date of the earliest data in the online archive.
node.$historicalDataConfiguration.startOfOnlineArchive.setValueFromSource({
dataType: DataType.DateTime, value: newDate
});
var startOfArchiveDataValue = _get_startOfOfflineArchive(node);
if (startOfArchiveDataValue.statusCode !== StatusCodes.Good || startOfArchiveDataValue.value.value.getTime() >= newDate.getTime()) {
node.$historicalDataConfiguration.startOfArchive.setValueFromSource({
dataType: DataType.DateTime, value: newDate
});
}
}
function _historyPush(newDataValue) {
var node = this;
assert(node.hasOwnProperty("historizing"),"expecting a historizing attribute on node");
if (!node.historizing) {
return; //
}
assert(node.historizing === true);
node._timeline.push(newDataValue);
var sourceTime = newDataValue.sourceTimestamp || new Date();
var sourcePicoSeconds = newDataValue.sourcePicoseconds || 0;
// ensure that values are set with date increasing
if (sourceTime.getTime() <= node.lastDate.getTime()) {
if (!(sourceTime.getTime() === node.lastDate.getTime() && sourcePicoSeconds > node.lastDatePicoSeconds)) {
console.log("Warning date not increasing ".red, newDataValue.toString(), " last known date = ", node.lastDate);
}
}
node.lastDate = sourceTime;
node.lastDatePicoSeconds = newDataValue.sourcePicoseconds || 0;
// we keep only a limited amount in main memory
if (node._timeline.length > node._maxOnlineValues) {
assert(_.isNumber(node._maxOnlineValues) && node._maxOnlineValues > 0);
while (node._timeline.length > node._maxOnlineValues) {
node._timeline.shift();
}
}
if (node._timeline.length >= node._maxOnlineValues || node._timeline.length === 1) {
var first = node._timeline.first();
_update_startOfOnlineArchive.call(node, first.sourceTimestamp);
//we update the node startOnlineDate
}
}
function _historyRead(context, historyReadDetails, indexRange, dataEncoding, continuationPoint, callback) {
//xx console.log("historyReadDetails = ", historyReadDetails.toString());
assert(context instanceof SessionContext);
assert(callback instanceof Function);
var node = this;
var dataValues = filter_dequeue(node._timeline, inInTimeRange.bind(null, historyReadDetails));
var result = new HistoryReadResult({
historyData: new HistoryData({dataValues: dataValues}),
statusCode: StatusCodes.Good
});
//xx console.log(" Results = ",result.toString());
callback(null, result);
}
var Dequeue = require("dequeue");
function on_value_change(newDataValue) {
var node = this;
node._historyPush.call(node, newDataValue);
}
/**
* @method installHistoricalDataNode
* @param node UAVariable
* @param [options] {Object}
* @param [options.maxOnlineValues = 1000]
*/
function installHistoricalDataNode(node,options) {
assert(node instanceof UAVariable);
options = options || {};
var addressSpace = node.addressSpace;
// install specific history behavior
node._historyRead = _historyRead;
node._historyPush = _historyPush;
node.lastDate = new Date(1600, 0, 1);
node._timeline = new Dequeue();
node._maxOnlineValues = options.maxOnlineValues || 1000;
var historicalDataConfigurationType = addressSpace.findObjectType("HistoricalDataConfigurationType");
node.historizing = true;
node.accessLevel = AccessLevelFlag.get(node.accessLevel.key + " | CurrentRead | HistoryRead");
node.userAccessLevel = AccessLevelFlag.get(node.userAccessLevel.key + " | CurrentRead | HistoryRead");
var optionals = [
"Definition",
"MaxTimeInterval",
"MinTimeInterval",
"StartOfArchive",
"StartOfOnlineArchive"
];
// Note from spec : If a HistoricalDataNode has configuration defined then one
// instance shall have a BrowseName of ‘HA Configuration’
var historicalDataConfiguration = historicalDataConfigurationType.instantiate({
browseName: "HA Configuration",
optionals: optionals,
});
// All Historical Configuration Objects shall be referenced using the HasHistoricalConfiguration ReferenceType.
node.addReference({
referenceType: "HasHistoricalConfiguration",
isForward: true,
nodeId: historicalDataConfiguration.nodeId
});
// The Stepped Variable specifies whether the historical data was collected in such a manner
// that it should be displayed as SlopedInterpolation (sloped line between points) or as
// SteppedInterpolation (vertically-connected horizontal lines between points) when raw data is
// examined. This Property also effects how some Aggregates are calculated. A value of True
// indicates the stepped interpolation mode. A value of False indicates SlopedInterpolation
// mode. The default value is False.
historicalDataConfiguration.stepped.setValueFromSource({dataType: "Boolean", value: false});
// The MaxTimeInterval Variable specifies the maximum interval between data points in the
// history repository regardless of their value change (see Part 3 for definition of Duration).
historicalDataConfiguration.maxTimeInterval.setValueFromSource({dataType: "Duration", value: 10*1000});
// The MinTimeInterval Variable specifies the minimum interval between data points in the
// history repository regardless of their value change
historicalDataConfiguration.minTimeInterval.setValueFromSource({dataType: "Duration", value: 0.1*1000});
// The StartOfArchive Variable specifies the date before which there is no data in the archive
// either online or offline.
// The StartOfOnlineArchive Variable specifies the date of the earliest data in the online archive.
var startOfOnlineArchive = new Date(Date.now);
historicalDataConfiguration.startOfOnlineArchive.setValueFromSource({
dataType: DataType.DateTime,
value: startOfOnlineArchive
});
//TreatUncertainAsBad
// The TreatUncertainAsBad Variable indicates how the Server treats data returned with a
// StatusCode severity Uncertain with respect to Aggregate calculations. A value of True indicates
// the Server considers the severity equivalent to Bad, a value of False indicates the Server
// considers the severity equivalent to Good, unless the Aggregate definition says otherwise. The
// default value is True. Note that the value is still treated as Uncertain when the StatusCode for
// the result is calculated.
historicalDataConfiguration.aggregateConfiguration.treatUncertainAsBad.setValueFromSource({dataType: "Boolean", value: true});
// The PercentDataBad Variable indicates the minimum percentage of Bad data in a given interval required for the
// StatusCode for the given interval for processed data request to be set to Bad.
// (Uncertain is treated as defined above.) Refer to 5.4.3 for details on using this Variable when assigning
// StatusCodes. For details on which Aggregates use the PercentDataBad Variable, see
// the definition of each Aggregate. The default value is 100.
historicalDataConfiguration.aggregateConfiguration.percentDataBad.setValueFromSource({dataType:"Byte",value:100});
// The PercentDataGood Variable indicates the minimum percentage of Good data in a given
// interval required for the StatusCode for the given interval for the processed data requests to be
// set to Good. Refer to 5.4.3 for details on using this Variable when assigning StatusCodes. For
// details on which Aggregates use the PercentDataGood Variable, see the definition of each
// Aggregate. The default value is 100.
historicalDataConfiguration.aggregateConfiguration.percentDataGood.setValueFromSource({dataType:"Byte",value:100});
//
// The PercentDataGood and PercentDataBad shall follow the following relationship
// PercentDataGood ≥ (100 – PercentDataBad). If they are equal the result of the
// PercentDataGood calculation is used. If the values entered for PercentDataGood and
//
// PercentDataBad do not result in a valid calculation (e.g. Bad = 80; Good = 0) the result will
// have a StatusCode of Bad_AggregateInvalidInputs The StatusCode
//
// Bad_AggregateInvalidInputs will be returned if the value of PercentDataGood or
// PercentDataBad exceed 100.
node.$historicalDataConfiguration = historicalDataConfiguration;
var dataValue = node.readValue();
if (dataValue.statusCode !== StatusCodes.BadWaitingForInitialData) {
on_value_change.call(node,dataValue);
}
node.on("value_changed",on_value_change);
// update the index of historizing nodes in the addressSpace
node.addressSpace.historizingNodes = node.addressSpace.historizingNodes || {};
node.addressSpace.historizingNodes[node.nodeId.toString()] = node;
}
AddressSpace.prototype.installHistoricalDataNode = installHistoricalDataNode;
};