APIs

Show:
"use strict";
/**
 * @module opcua.server
 *
 */


const assert = require("node-opcua-assert").assert;
const _ = require("underscore");

const EventEmitter = require("events").EventEmitter;
const util = require("util");

const subscription_service = require("node-opcua-service-subscription");
const read_service = require("node-opcua-service-read");

const DataValue = require("node-opcua-data-value").DataValue;
const Variant = require("node-opcua-variant").Variant;
const StatusCodes = require("node-opcua-status-code").StatusCodes;

const AttributeIds = require("node-opcua-data-model").AttributeIds;
const BaseNode = require("node-opcua-address-space").BaseNode;

const sameDataValue = require("node-opcua-data-value").sameDataValue;
const sameVariant = require("node-opcua-variant/src/variant_tools").sameVariant;
const isValidVariant = require("node-opcua-variant").isValidVariant;

const MonitoringMode = subscription_service.MonitoringMode;
const MonitoringParameters = subscription_service.MonitoringParameters;
const MonitoredItemModifyResult = subscription_service.MonitoredItemModifyResult;
const TimestampsToReturn = read_service.TimestampsToReturn;
const EventFilter = require("node-opcua-service-filter").EventFilter;
const apply_timestamps = require("node-opcua-data-value").apply_timestamps;

const UAVariable = require("node-opcua-address-space").UAVariable;

const defaultItemToMonitor = {indexRange: null, attributeId: read_service.AttributeIds.Value};

const SessionContext = require("node-opcua-address-space").SessionContext;
const NumericRange = require("node-opcua-numeric-range").NumericRange;



const debugLog = require("node-opcua-debug").make_debugLog(__filename);
const doDebug = require("node-opcua-debug").checkDebugFlag(__filename);

function _adjust_sampling_interval(samplingInterval,node_minimumSamplingInterval) {

    assert(_.isNumber(node_minimumSamplingInterval,"expecting a number"));

    if (samplingInterval === 0) {
        return (node_minimumSamplingInterval === 0) ? samplingInterval : Math.max(MonitoredItem.minimumSamplingInterval,node_minimumSamplingInterval);
    }
    assert(samplingInterval >= 0, " this case should have been prevented outside");
    samplingInterval = samplingInterval || MonitoredItem.defaultSamplingInterval;
    samplingInterval = Math.max(samplingInterval, MonitoredItem.minimumSamplingInterval);
    samplingInterval = Math.min(samplingInterval, MonitoredItem.maximumSamplingInterval);
    samplingInterval = node_minimumSamplingInterval === 0 ? samplingInterval : Math.max(samplingInterval, node_minimumSamplingInterval);

    return samplingInterval;
}


const maxQueueSize = 5000;

function _adjust_queue_size(queueSize) {
    queueSize = Math.min(queueSize, maxQueueSize);
    queueSize = Math.max(1, queueSize);
    return queueSize;
}


/**
 * a server side monitored item
 *
 * - Once created, the MonitoredItem will raised an "samplingEvent" event every "samplingInterval" millisecond
 *   until {{#crossLink "MonitoredItem/terminate:method"}}{{/crossLink}} is called.
 *
 * - It is up to the  event receiver to call {{#crossLink "MonitoredItem/recordValue:method"}}{{/crossLink}}.
 *
 * @class MonitoredItem
 * @param options  the options
 * @param options.clientHandle     {Number}  - the client handle
 * @param options.samplingInterval {Number}  - the sampling Interval
 * @param options.filter           {ExtensionObject}
 * @param options.discardOldest    {boolean} - if discardOldest === true, older items are removed from the queue
 *                                             when
 * @param options.queueSize        {Number} - the size of the queue.
 * @param options.monitoredItemId  {Number} the monitoredItem Id assigned by the server to this monitoredItem.
 * @param options.itemToMonitor
 * @param options.monitoringMode
 * @param options.timestampsToReturn
 * @constructor
 */
function MonitoredItem(options) {

    assert(options.hasOwnProperty("monitoredItemId"));
    assert(!options.monitoringMode, "use setMonitoring mode explicitly to activate the monitored item");

    EventEmitter.apply(this, arguments);
    options.itemToMonitor = options.itemToMonitor || defaultItemToMonitor;

    const self = this;
    self._samplingId = null;

    self._set_parameters(options);

    self.monitoredItemId = options.monitoredItemId; //( known as serverHandle)

    self.queue = [];
    self.overflow = false;

    self.oldDataValue = new DataValue({statusCode: StatusCodes.BadDataUnavailable}); // unset initially

    // user has to call setMonitoringMode
    self.monitoringMode = MonitoringMode.Invalid;

    self.timestampsToReturn = options.timestampsToReturn || TimestampsToReturn.Neither;

    self.itemToMonitor = options.itemToMonitor;

    self.filter = options.filter || null;

    /**
     * @property node the associated node object in the address space
     * @type {BaseNode|null}
     */
    self._node = null;
    self._semantic_version = 0;

    if (doDebug) {
        debugLog("Monitoring ", options.itemToMonitor.toString());
    }

    self._on_node_disposed = null;

    MonitoredItem.registry.register(self);
}

util.inherits(MonitoredItem, EventEmitter);

const ObjectRegistry = require("node-opcua-object-registry").ObjectRegistry;
MonitoredItem.registry = new ObjectRegistry();

MonitoredItem.minimumSamplingInterval = 50;              // 50 ms as a minimum sampling interval
MonitoredItem.defaultSamplingInterval = 1500;            // 1500 ms as a default sampling interval
MonitoredItem.maximumSamplingInterval = 1000 * 60 * 60;  // 1 hour !

function _validate_parameters(monitoringParameters) {
    //xx assert(options instanceof MonitoringParameters);
    assert(monitoringParameters.hasOwnProperty("clientHandle"));
    assert(monitoringParameters.hasOwnProperty("samplingInterval"));
    assert(_.isFinite(monitoringParameters.clientHandle));
    assert(_.isFinite(monitoringParameters.samplingInterval));
    assert(_.isBoolean(monitoringParameters.discardOldest));
    assert(_.isFinite(monitoringParameters.queueSize));
    assert(monitoringParameters.queueSize >= 0);
}

MonitoredItem.prototype.__defineGetter__("node", function () {
    return this._node;
});

MonitoredItem.prototype.__defineSetter__("node", function () {
    throw new Error("Unexpected way to set node");
});



function _on_node_disposed(monitoredItem) {
    const node = this;
    monitoredItem._on_value_changed(new DataValue({sourceTimestamp: new Date(),statusCode: StatusCodes.BadNodeIdInvalid}));
    monitoredItem._stop_sampling();

}

MonitoredItem.prototype.setNode = function (node) {
    const self = this;
    assert(!self.node || self.node === node, "node already set");
    self._node = node;
    self._semantic_version = node.semantic_version;


    assert( self._on_node_disposed ===null,"setNode has been called already ???");
    self._on_node_disposed = _on_node_disposed.bind(null,self);
    self._node.on("dispose",self._on_node_disposed);

};

MonitoredItem.prototype._stop_sampling = function () {

    const self = this;
    debugLog("MonitoredItem#_stop_sampling");

    if (self._on_opcua_event_received_callback) {
        assert(_.isFunction(self._on_opcua_event_received_callback));
        self.node.removeListener("event", self._on_opcua_event_received_callback);
        self._on_opcua_event_received_callback = null;
    }

    if (self._attribute_changed_callback) {
        assert(_.isFunction(self._attribute_changed_callback));

        const event_name = BaseNode.makeAttributeEventName(self.itemToMonitor.attributeId);
        self.node.removeListener(event_name, self._attribute_changed_callback);
        self._attribute_changed_callback = null;
    }

    if (self._value_changed_callback) {
        // samplingInterval was 0 for a exception-based data Item
        // we setup a event listener that we need to unwind here
        assert(_.isFunction(self._value_changed_callback));
        assert(!self._samplingId);

        self.node.removeListener("value_changed", self._value_changed_callback);
        self._value_changed_callback = null;
    }

    if (self._semantic_changed_callback) {
        assert(_.isFunction(self._semantic_changed_callback));
        assert(!self._samplingId);
        self.node.removeListener("semantic_changed", self._semantic_changed_callback);
        self._semantic_changed_callback = null;

    }
    if (self._samplingId) {
        self._clear_timer();
    }

    assert(!self._samplingId);
    assert(!self._value_changed_callback);
    assert(!self._semantic_changed_callback);
    assert(!self._attribute_changed_callback);
    assert(!self._on_opcua_event_received_callback);

};

MonitoredItem.prototype._on_value_changed = function (dataValue,indexRange) {
    const self = this;
    assert(dataValue instanceof DataValue);
    self.recordValue(dataValue, false, indexRange);
};

MonitoredItem.prototype._on_semantic_changed = function () {
    const self = this;
    const dataValue = self.node.readValue();
    self._on_value_changed(dataValue);
};

const extractEventFields = require("node-opcua-service-filter").extractEventFields;

MonitoredItem.prototype._on_opcua_event = function (eventData) {

    const self = this;

    assert(!self.filter || self.filter instanceof EventFilter);

    const selectClauses = self.filter ? self.filter.selectClauses : [];
    const eventFields = extractEventFields(selectClauses, eventData);

    // istanbul ignore next
    if (doDebug) {
        console.log(" RECEIVED INTERNAL EVENT THAT WE ARE MONITORING");
        console.log(self.filter ? self.filter.toString() : "no filter");
        eventFields.forEach(function (e) {
            console.log(e.toString());
        });
    }

    self._enqueue_event(eventFields);
};

MonitoredItem.prototype._getSession = function () {
    const self = this;
    if (!self.$subscription) {
        return null;
    }
    if (!self.$subscription.$session) {
        return null;
    }
    return self.$subscription.$session;
};

MonitoredItem.prototype._start_sampling = function (recordInitialValue) {

    const self = this;

    // make sure oldDataValue is scrapped so first data recording can happen
    self.oldDataValue = new DataValue({statusCode: StatusCodes.BadDataUnavailable}); // unset initially

    self._stop_sampling();


    const context = new SessionContext({session: self._getSession(),server: self.server});

    if (self.itemToMonitor.attributeId === AttributeIds.EventNotifier) {

        // istanbul ignore next
        if (doDebug) {
            debugLog("xxxxxx monitoring EventNotifier on", self.node.nodeId.toString(), self.node.browseName.toString());
        }
        // we are monitoring OPCUA Event
        self._on_opcua_event_received_callback = self._on_opcua_event.bind(self);
        self.node.on("event", self._on_opcua_event_received_callback);

        return;
    }
    if (self.itemToMonitor.attributeId !== AttributeIds.Value) {

        // sampling interval only applies to Value Attributes.
        self.samplingInterval = 0; // turned to exception-based regardless of requested sampling interval

        // non value attribute only react on value change
        self._attribute_changed_callback = self._on_value_changed.bind(this);
        const event_name = BaseNode.makeAttributeEventName(self.itemToMonitor.attributeId);

        self.node.on(event_name, self._attribute_changed_callback);

        if (recordInitialValue) {
            // read initial value
            const dataValue = self.node.readAttribute(context, self.itemToMonitor.attributeId);
            self.recordValue(dataValue, true, null);

        }
        return;
    }

    if (self.samplingInterval === 0) {

        // we have a exception-based dataItem : event based model, so we do not need a timer
        // rather , we setup the "value_changed_event";
        self._value_changed_callback = self._on_value_changed.bind(this);
        self._semantic_changed_callback = self._on_semantic_changed.bind(this);

        self.node.on("value_changed", self._value_changed_callback);
        self.node.on("semantic_changed", self._semantic_changed_callback);

        // initiate first read
        if (recordInitialValue) {
//xx            setImmediate(function() {
            self.node.readValueAsync(context, function (err, dataValue) {
                self.recordValue(dataValue, true);
            });
//xx            });
        }
    } else {

        self._set_timer();
        if (recordInitialValue) {
            setImmediate(function () {
                //xx console.log("Record Initial Value ",self.node.nodeId.toString());
                // initiate first read (this requires self._samplingId to be set)
                self._on_sampling_timer();
            });
        }
    }

};

MonitoredItem.prototype.setMonitoringMode = function (monitoringMode) {

    const self = this;

    assert(monitoringMode !== MonitoringMode.Invalid);

    if (monitoringMode === self.monitoringMode) {
        // nothing to do
        return;
    }

    const old_monitoringMode = self.monitoringMode;


    self.monitoringMode = monitoringMode;

    if (self.monitoringMode === MonitoringMode.Disabled) {

        self._stop_sampling();

        // OPCUA 1.03 part 4 : $5.12.4
        // setting the mode to DISABLED causes all queued Notifications to be deleted
        self.queue = [];
        self.overflow = false;
    } else {
        assert(self.monitoringMode === MonitoringMode.Sampling || self.monitoringMode === MonitoringMode.Reporting);

        // OPCUA 1.03 part 4 : $5.12.1.3
        // When a MonitoredItem is enabled (i.e. when the MonitoringMode is changed from DISABLED to
        // SAMPLING or REPORTING) or it is created in the enabled state, the Server shall report the first
        // sample as soon as possible and the time of this sample becomes the starting point for the next
        // sampling interval.
        const recordInitialValue = (old_monitoringMode === MonitoringMode.Invalid || old_monitoringMode === MonitoringMode.Disabled);

        self._start_sampling(recordInitialValue);

    }
};

MonitoredItem.prototype._set_parameters = function (monitoredParameters) {
    const self = this;
    _validate_parameters(monitoredParameters);
    self.clientHandle = monitoredParameters.clientHandle;

    // The Server may support data that is collected based on a sampling model or generated based on an
    // exception-based model. The fastest supported sampling interval may be equal to 0, which indicates
    // that the data item is exception-based rather than being sampled at some period. An exception-based
    // model means that the underlying system does not require sampling and reports data changes.
    if (self.node && self.node instanceof UAVariable) {
        self.samplingInterval = _adjust_sampling_interval(monitoredParameters.samplingInterval, self.node ? self.node.minimumSamplingInterval : 0);
    } else {
        self.samplingInterval = _adjust_sampling_interval(monitoredParameters.samplingInterval, 0);
    }
    self.discardOldest = monitoredParameters.discardOldest;
    self.queueSize = _adjust_queue_size(monitoredParameters.queueSize);
};

/**
 * Terminate the  MonitoredItem.
 * @method terminate
 *
 * This will stop the internal sampling timer.
 */
MonitoredItem.prototype.terminate = function () {
    const self = this;
    self._stop_sampling();
};

MonitoredItem.prototype.dispose = function() {
    const self = this;

    if (doDebug) {
        debugLog("DISPOSING MONITORED ITEM" , self._node.nodeId.toString());
    }

    self._stop_sampling();

    MonitoredItem.registry.unregister(self);

    if (self._on_node_disposed) {
        self._node.removeListener("dispose",self._on_node_disposed);
        self._on_node_disposed = null;
    }

    //x assert(self._samplingId === null,"Sampling Id must be null");
    self.oldDataValue = null;
    self.queue = null;
    self.itemToMonitor = null;
    self.filter = null;
    self.monitoredItemId = null;
    self._node = null;
    self._semantic_version = 0;

    self.$subscription = null;

    self.removeAllListeners();

    assert(!self._samplingId);
    assert(!self._value_changed_callback);
    assert(!self._semantic_changed_callback);
    assert(!self._attribute_changed_callback);
    assert(!self._on_opcua_event_received_callback);
    self._on_opcua_event_received_callback = null;
    self._attribute_changed_callback = null;
    self._semantic_changed_callback = null;
    self._on_opcua_event_received_callback = null;

};

/**
 * @method _on_sampling_timer
 * @private
 * request
 *
 */
MonitoredItem.prototype._on_sampling_timer = function () {

    const self = this;

    // istanbul ignore next
    if (doDebug) {
        debugLog("MonitoredItem#_on_sampling_timer", self.node ? self.node.nodeId.toString() : "null", " isSampling?=", self._is_sampling);
    }

    if (self._samplingId) {

        assert(self.monitoringMode === MonitoringMode.Sampling || self.monitoringMode === MonitoringMode.Reporting);

        if (self._is_sampling) {
            // previous sampling call is not yet completed..
            // there is nothing we can do about it except waiting until next tick.
            // note : see also issue #156 on github
            return;
        }
        //xx console.log("xxxx ON SAMPLING");
        assert(!self._is_sampling, "sampling func shall not be re-entrant !! fix it");

        assert(_.isFunction(self.samplingFunc));

        self._is_sampling = true;

        self.samplingFunc.call(self, self.oldDataValue, function (err, newDataValue) {

            if(!self._samplingId) {
                // item has been dispose .... the monitored item has been disposed while the async sampling func
                // was taking place ... just ignore this
                return;
            }
            if (err) {
                console.log(" SAMPLING ERROR =>", err);
            } else {
                // only record value if source timestamp is newer
                //xx if (newDataValue.sourceTimestamp > self.oldDataValue.sourceTimestamp) {
                self._on_value_changed(newDataValue);
                //xx }
            }
            self._is_sampling = false;
        });

    } else {
        /* istanbul ignore next */
        debugLog("_on_sampling_timer call but MonitoredItem has been terminated !!! ");
    }
};

const extractRange = require("node-opcua-data-value").extractRange;

const DataChangeFilter = subscription_service.DataChangeFilter;
const DataChangeTrigger = subscription_service.DataChangeTrigger;
const DeadbandType = subscription_service.DeadbandType;

function statusCodeHasChanged(newDataValue, oldDataValue) {
    assert(newDataValue instanceof DataValue);
    assert(oldDataValue instanceof DataValue);
    return newDataValue.statusCode !== oldDataValue.statusCode;
}

const check_deadband = require("node-opcua-service-subscription").check_deadband;


function valueHasChanged(self, newDataValue, oldDataValue, deadbandType, deadbandValue) {

    assert(newDataValue instanceof DataValue);
    assert(oldDataValue instanceof DataValue);
    switch (deadbandType) {
        case DeadbandType.None:
            assert(newDataValue.value instanceof Variant);
            assert(newDataValue.value instanceof Variant);
            // No Deadband calculation should be applied.
            return check_deadband(oldDataValue.value, newDataValue.value, DeadbandType.None);
        case DeadbandType.Absolute:
            // AbsoluteDeadband
            return check_deadband(oldDataValue.value, newDataValue.value, DeadbandType.Absolute, deadbandValue);
        default:
            // Percent_2    PercentDeadband (This type is specified in Part 8).
            assert(deadbandType === DeadbandType.Percent);

            // The range of the deadbandValue is from 0.0 to 100.0 Percent.
            assert(deadbandValue >= 0 && deadbandValue <= 100);

            // DeadbandType = PercentDeadband
            // For this type of deadband the deadbandValue is defined as the percentage of the EURange. That is,
            // it applies only to AnalogItems with an EURange Property that defines the typical value range for the
            // item. This range shall be multiplied with the deadbandValue and then compared to the actual value change
            // to determine the need for a data change notification. The following pseudo code shows how the deadband
            // is calculated:
            //      DataChange if (absolute value of (last cached value - current value) >
            //                                          (deadbandValue/100.0) * ((high-low) of EURange)))
            //
            // Specifying a deadbandValue outside of this range will be rejected and reported with the
            // StatusCode Bad_DeadbandFilterInvalid (see Table 27).
            // If the Value of the MonitoredItem is an array, then the deadband calculation logic shall be applied to
            // each element of the array. If an element that requires a DataChange is found, then no further
            // deadband checking is necessary and the entire array shall be returned.
            assert(self.node !== null, "expecting a valid address_space object here to get access the the EURange");

            if (self.node.euRange) {
                // double,double
                const rangeVariant = self.node.euRange.readValue().value;
                const range = rangeVariant.value.high - rangeVariant.value.high;
                assert(_.isFinite(range));
                return check_deadband(oldDataValue.value, newDataValue.value, DeadbandType.Percent, deadbandValue, range);

            }
            return true;
    }
}

function timestampHasChanged(t1, t2) {
    if ((t1 || !t2) || (t2 || !t1)) {
        return true;
    }
    if (!t1 || !t2) {
        return false;
    }
    return t1.getTime() !== t2.getTime();
}

function apply_datachange_filter(self, newDataValue, oldDataValue) {

    assert(self.filter);
    assert(self.filter instanceof DataChangeFilter);
    assert(newDataValue instanceof DataValue);
    assert(oldDataValue instanceof DataValue);

    const trigger = self.filter.trigger;

    switch (trigger.value) {
        case DataChangeTrigger.Status.value: // Status
            //              Report a notification ONLY if the StatusCode associated with
            //              the value changes. See Table 166 for StatusCodes defined in
            //              this standard. Part 8 specifies additional StatusCodes that are
            //              valid in particular for device data.
            return statusCodeHasChanged(newDataValue, oldDataValue);

        case DataChangeTrigger.StatusValue.value: // StatusValue
            //              Report a notification if either the StatusCode or the value
            //              change. The Deadband filter can be used in addition for
            //              filtering value changes.
            //              This is the default setting if no filter is set.
            return statusCodeHasChanged(newDataValue, oldDataValue) ||
                valueHasChanged(self, newDataValue, oldDataValue, self.filter.deadbandType, self.filter.deadbandValue);

        default: // StatusValueTimestamp
            //              Report a notification if either StatusCode, value or the
            //              SourceTimestamp change.
            //
            //              If a Deadband filter is specified,this trigger has the same behaviour as STATUS_VALUE_1.
            //
            //              If the DataChangeFilter is not applied to the monitored item, STATUS_VALUE_1
            //              is the default reporting behaviour
            assert(trigger === DataChangeTrigger.StatusValueTimestamp);
            return timestampHasChanged(newDataValue.sourceTimestamp, oldDataValue.sourceTimestamp) ||
                statusCodeHasChanged(newDataValue, oldDataValue) ||
                valueHasChanged(self, newDataValue, oldDataValue, self.filter.deadbandType, self.filter.deadbandValue);
    }
    return false;
}

function apply_filter(self, newDataValue) {

    if (!self.oldDataValue) {
        return true; // keep
    }
    if (self.filter instanceof DataChangeFilter) {
        return apply_datachange_filter(self, newDataValue, self.oldDataValue);
    }
    return true; // keep
    // else {
    //      // if filter not set, by default report changes to Status or Value only
    //      return !sameDataValue(newDataValue, self.oldDataValue, TimestampsToReturn.Neither);
    // }
    // return true; // keep
}


/**
 * @property isSampling
 * @type boolean
 */
MonitoredItem.prototype.__defineGetter__("isSampling", function () {
    const self = this;
    return !!self._samplingId || _.isFunction(self._value_changed_callback) ||
        _.isFunction(self._attribute_changed_callback);
});

/**
 * @method recordValue
 * @param dataValue {DataValue}     the whole dataValue
 * @param skipChangeTest {Boolean}  indicates whether recordValue should  not check that dataValue is really different
 *                                  from previous one, ( by checking timestamps but also variant value)
 * @private
 *
 * Notes:
 *  - recordValue can only be called within timer event
 *  - for performance reason, dataValue may be a shared value with the underlying node,
 *    therefore recordValue must clone the dataValue to make sure it retains a snapshot
 *    of the contain at the time recordValue was called.
 *
 */
MonitoredItem.prototype.recordValue = function (dataValue, skipChangeTest , indexRange) {

    const self = this;

    assert(dataValue instanceof DataValue);
    assert(dataValue !== self.oldDataValue,               "recordValue expects different dataValue to be provided");
    assert(!dataValue.value || dataValue.value !== self.oldDataValue.value,   "recordValue expects different dataValue.value to be provided");
    assert(!dataValue.value  || dataValue.value.isValid(),"expecting a valid variant value");

    const hasSemanticChanged = self.node && (self.node.semantic_version !== self._semantic_version);

    //xx   console.log("`\n----------------------------",skipChangeTest,self.clientHandle,
    //             self.node.listenerCount("value_changed"),self.node.nodeId.toString());
    //xx   console.log("events ---- ",self.node.eventNames().join("-"));
    //xx    console.log((new Error()).stack);
    //xx    console.log("indexRange = ",indexRange ? indexRange.toString() :"");
    //xx    console.log("self.itemToMonitor.indexRange = ",self.itemToMonitor.indexRange.toString());

    if (!hasSemanticChanged && indexRange && self.itemToMonitor.indexRange) {
        // we just ignore changes that do not fall within our range
        // ( unless semantic bit has changed )
        if (!NumericRange.overlap(indexRange,self.itemToMonitor.indexRange)) {
            return; // no overlap !
        }
    }

    assert( self.itemToMonitor,"must have a valid itemToMonitor(have this monitoredItem been disposed already ?");
    // extract the range that we are interested with
    dataValue = extractRange(dataValue, self.itemToMonitor.indexRange);

    // istanbul ignore next
    if (doDebug) {
        debugLog("MonitoredItem#recordValue", self.node.nodeId.toString(), self.node.browseName.toString(), " has Changed = ", !sameDataValue(dataValue, self.oldDataValue));
    }

    // if semantic has changed, value need to be enqueued regardless of other assumptions
    if (hasSemanticChanged) {
        return self._enqueue_value(dataValue);
    }

    const useIndexRange = self.itemToMonitor.indexRange && !self.itemToMonitor.indexRange.isEmpty();

    if (!skipChangeTest) {
        const hasChanged = !sameDataValue(dataValue, self.oldDataValue);
        if (!hasChanged) {
            return;
        }
    }

    if (!apply_filter(self, dataValue)) {
        return;
    }

    if (useIndexRange) {
        // when an indexRange is provided , make sure that no record happens unless
        // extracted variant in the selected range  has really changed.

        // istanbul ignore next
        if (doDebug) {
            debugLog("Current : ",self.oldDataValue.toString());
            debugLog("New : ",dataValue.toString());
            debugLog("indexRange=",indexRange);
        }

        if (sameVariant(dataValue.value, self.oldDataValue.value)) {
            return;
        }
    }
    // store last value
    self._enqueue_value(dataValue);

};

MonitoredItem.prototype._setOverflowBit = function (notification) {

    if (notification.hasOwnProperty("value")) {
        assert(notification.value.statusCode.equals(StatusCodes.Good));
        notification.value.statusCode = StatusCodes.makeStatusCode(notification.value.statusCode, "Overflow | InfoTypeDataValue");
        assert(_.isEqual(notification.value.statusCode, StatusCodes.GoodWithOverflowBit));
        assert(notification.value.statusCode.hasOverflowBit);
    }
};

function setSemanticChangeBit(notification) {
    if (notification && notification.hasOwnProperty("value")) {
        notification.value.statusCode = StatusCodes.makeStatusCode(notification.value.statusCode, "SemanticChanged");
    }
}

MonitoredItem.prototype._enqueue_notification = function (notification) {

    const self = this;

    if (self.queueSize === 1) {
        // ensure queuesize
        if (!self.queue || self.queue.length !== 1) {
            self.queue = [null];
        }
        self.queue[0] = notification;
        assert(self.queue.length === 1);

    } else {
        if (self.discardOldest) {

            // push new value to queue
            self.queue.push(notification);

            if (self.queue.length > self.queueSize) {

                self.overflow = true;

                self.queue.shift(); // remove front element

                // set overflow bit
                self._setOverflowBit(self.queue[0]);
            }

        } else {
            if (self.queue.length < self.queueSize) {

                self.queue.push(notification);
            } else {

                self.overflow = true;

                self._setOverflowBit(notification);

                self.queue[self.queue.length - 1] = notification;
            }
        }
    }
    assert(self.queue.length >= 1);
};


MonitoredItem.prototype._makeDataChangeNotification = function (dataValue) {
    const self = this;
    const attributeId = self.itemToMonitor.attributeId;
    dataValue = apply_timestamps(dataValue, self.timestampsToReturn, attributeId);
    return new subscription_service.MonitoredItemNotification({clientHandle: self.clientHandle, value: dataValue});
};

function isGoodish(statusCode) {
    return statusCode.value < 0x10000000;
}


/**
 * @method _enqueue_value
 * @param dataValue {DataValue} the dataValue to enquue
 * @private
 */
MonitoredItem.prototype._enqueue_value = function (dataValue) {

    const self = this;

    // preconditions:

    assert(dataValue instanceof DataValue);
    // lets verify that, if status code is good then we have a valid Variant in the dataValue
    assert(!isGoodish(dataValue.statusCode) || dataValue.value instanceof Variant);
    //xx assert(isGoodish(dataValue.statusCode) || util.isNullOrUndefined(dataValue.value) );
    // let's check that data Value is really a different object
    // we may end up with corrupted queue if dataValue are recycled and stored as is in notifications
    assert(dataValue !== self.oldDataValue, "dataValue cannot be the same object twice!");


    //Xx // todo ERN !!!! PLEASE CHECK THIS !!!
    //Xx // let make a clone, so we have a snapshot
    //Xx dataValue = dataValue.clone();


    // let's check that data Value is really a different object
    // we may end up with corrupted queue if dataValue are recycled and stored as is in notifications
    assert(!self.oldDataValue || !dataValue.value || (dataValue.value !== self.oldDataValue.value), "dataValue cannot be the same object twice!");
    if (!(!self.oldDataValue || !self.oldDataValue.value
            || !dataValue.value || !(dataValue.value.value instanceof Object)
            || (dataValue.value.value !== self.oldDataValue.value.value)) && !(dataValue.value.value instanceof Date)) {
        throw new Error("dataValue.value.value cannot be the same object twice! " + self.node.browseName.toString() + " " + dataValue.toString()  + "  " + self.oldDataValue.toString().cyan);
    }

    // istanbul ignore next
    if (doDebug) {
        debugLog("MonitoredItem#_enqueue_value", self.node.nodeId.toString());
    }

    self.oldDataValue = dataValue;
    const notification = self._makeDataChangeNotification(dataValue);
    self._enqueue_notification(notification);
};

MonitoredItem.prototype._makeEventFieldList = function (eventFields) {
    const self = this;
    assert(_.isArray(eventFields));
    return new subscription_service.EventFieldList({clientHandle: self.clientHandle, eventFields: eventFields});
};

MonitoredItem.prototype._enqueue_event = function (eventFields) {
    const self = this;
    debugLog(" MonitoredItem#_enqueue_event");
    const notification = self._makeEventFieldList(eventFields);
    self._enqueue_notification(notification);
};


MonitoredItem.prototype._empty_queue = function () {
    const self = this;
    // empty queue
    self.queue = [];
    self.overflow = false;

};



MonitoredItem.prototype.__defineGetter__("hasMonitoredItemNotifications", function () {
    const self = this;
    return self.queue.length > 0;
});

/**
 * @method  extractMonitoredItemNotifications
 * @return {Array.<*>}
 */
MonitoredItem.prototype.extractMonitoredItemNotifications = function () {

    const self = this;

    if (self.monitoringMode !== MonitoringMode.Reporting) {
        return [];
    }
    const notifications = self.queue;
    self._empty_queue();

    // apply semantic changed bit if necessary
    if (notifications.length > 0 && self.node && self._semantic_version < self.node.semantic_version) {

        const dataValue = notifications[notifications.length - 1];
        setSemanticChangeBit(dataValue);
        self._semantic_version = self.node.semantic_version;
    }

    return notifications;
};


const timers = {};

function appendToTimer(monitoredItem) {

    const samplingInterval = monitoredItem.samplingInterval;
    const key = samplingInterval.toString();
    assert(samplingInterval > 0);
    let _t = timers[key];
    if (!_t) {

        _t = {
            monitoredItems: {},
            monitoredItemsCount: 0,
            _samplingId: false
        };

        _t._samplingId = setInterval(function () {

            _.forEach(_t.monitoredItems, function (m) {
                setImmediate(function () {
                    m._on_sampling_timer();
                });
            });

        }, samplingInterval);
        timers[key] = _t;
    }
    assert(!_t.monitoredItems[monitoredItem.monitoredItemId]);
    _t.monitoredItems[monitoredItem.monitoredItemId] = monitoredItem;
    _t.monitoredItemsCount++;
    return key;
}

function removeFromTimer(monitoredItem) {

    const samplingInterval = monitoredItem.samplingInterval;
    assert(samplingInterval > 0);
    const key = monitoredItem._samplingId;
    const _t = timers[key];
    if (!_t) {
        console.log("cannot find common timer for samplingInterval", key);
        return;
    }
    assert(_t);
    assert(_t.monitoredItems[monitoredItem.monitoredItemId]);
    delete _t.monitoredItems[monitoredItem.monitoredItemId];
    _t.monitoredItemsCount--;
    assert(_t.monitoredItemsCount >= 0);
    if (_t.monitoredItemsCount === 0) {
        clearInterval(_t._samplingId);
        delete timers[key];
    }
}

const useCommonTimer = true;
MonitoredItem.prototype._clear_timer = function () {

    const self = this;

    //xx console.log("MonitoredItem#_clear_timer",self._samplingId);
    if (self._samplingId) {
        if (useCommonTimer) {
            removeFromTimer(self);
        } else {
            clearInterval(self._samplingId);
        }
        self._samplingId = null;
    }
};

MonitoredItem.prototype._set_timer = function () {

    const self = this;
    assert(self.samplingInterval >= MonitoredItem.minimumSamplingInterval);
    assert(!self._samplingId);

    if (useCommonTimer) {
        self._samplingId = appendToTimer(self);
    } else {
        // settle periodic sampling
        self._samplingId = setInterval(function () {
            self._on_sampling_timer();
        }, self.samplingInterval);
    }
    //xx console.log("MonitoredItem#_set_timer",self._samplingId);

};


MonitoredItem.prototype._adjust_queue_to_match_new_queue_size = function () {

    const self = this;

    // adjust queue size if necessary
    if (self.queueSize < self.queue.length) {

        if (self.discardOldest) {

            self.queue.splice(0, self.queue.length - self.queueSize);

        } else {

            const lastElement = self.queue[self.queue.length - 1];
            // only keep queueSize first element, discard others
            self.queue.splice(self.queueSize);
            self.queue[self.queue.length - 1] = lastElement;
        }
    }
    if (self.queueSize <= 1) {
        self.overflow = false;
        // unset OverFlowBit
        if (self.queue.length === 1) {
            if (self.queue[0].value) {
                if (self.queue[0].value.statusCode.hasOverflowBit) {
                    self.queue[0].value.statusCode.unset("Overflow | InfoTypeDataValue");
                }
            }
        }
    }
    assert(self.queue.length <= self.queueSize);
};

MonitoredItem.prototype._adjust_sampling = function (old_samplingInterval) {

    const self = this;
    if (old_samplingInterval !== self.samplingInterval) {
        self._start_sampling();
        //xx self._clear_timer(true);
        //xx self._set_timer();
    }
};

const validateFilter = require("./validate_filter").validateFilter;

MonitoredItem.prototype.modify = function (timestampsToReturn, monitoredParameters) {

    assert(monitoredParameters instanceof MonitoringParameters);

    const self = this;

    const old_samplingInterval = self.samplingInterval;

    self.timestampsToReturn = timestampsToReturn || self.timestampsToReturn;

    if (old_samplingInterval !== 0 && monitoredParameters.samplingInterval === 0) {
        monitoredParameters.samplingInterval = MonitoredItem.minimumSamplingInterval; // fastest possible
    }

    self._set_parameters(monitoredParameters);

    self._adjust_queue_to_match_new_queue_size();

    self._adjust_sampling(old_samplingInterval);


    if (monitoredParameters.filter) {
        const statusCodeFilter = validateFilter(monitoredParameters.filter, self.itemToMonitor, self.node);
        if (statusCodeFilter.isNot(StatusCodes.Good)) {
            return new MonitoredItemModifyResult({
                statusCode: statusCodeFilter
            });
        }
    }

    // validate filter
    // note : The DataChangeFilter does not have an associated result structure.
    const filterResult = null; // new subscription_service.DataChangeFilter

    return new MonitoredItemModifyResult({
        statusCode: StatusCodes.Good,
        revisedSamplingInterval: self.samplingInterval,
        revisedQueueSize: self.queueSize,
        filterResult: filterResult
    });
};

exports.MonitoredItem = MonitoredItem;