APIs

Show:
"use strict";
/**
 * @module opcua.address_space
 * @class AddressSpace
 */

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


const BrowseDirection = require("node-opcua-data-model").BrowseDirection;


const Variant = require("node-opcua-variant").Variant;
const DataType = require("node-opcua-variant").DataType;

const StatusCodes = require("node-opcua-status-code").StatusCodes;

const UAVariable = require("./ua_variable").UAVariable;
const Namespace = require("./namespace").Namespace;

// Release 1.03 12 OPC Unified Architecture, Part 9
// Two-state state machines
// Most states defined in this standard are simple – i.e. they are either TRUE or FALSE. The
// TwoStateVariableType is introduced specifically for this use case. More complex states are
// modelled by using a StateMachineType defined in Part 5.
// The TwoStateVariableType is derived from the StateVariableType.
//
// Attribute        Value
// BrowseName       TwoStateVariableType
// DataType         LocalizedText
// ValueRank        -1 (-1 = Scalar)
// IsAbstract       False
//
// Subtype of the StateVariableType defined in Part 5.
// Note that a Reference to this subtype is not shown in the definition of the StateVariableType
//
// References      NodeClass BrowseName              DataType      TypeDefinition Modelling Rule
// HasProperty     Variable  Id                      Boolean       PropertyType   Mandatory
// HasProperty     Variable  TransitionTime          UtcTime       PropertyType   Optional
// HasProperty     Variable  EffectiveTransitionTime UtcTime       PropertyType   Optional
// HasProperty     Variable  TrueState               LocalizedText PropertyType   Optional
// HasProperty     Variable  FalseState              LocalizedText PropertyType   Optional
// HasTrueSubState StateMachine or
//                 TwoStateVariableType
//                                                  <StateIdentifier> Defined in Clause 5.4.2 Optional
// HasFalseSubState StateMachine or
//                  TwoStateVariableType
//                                                  <StateIdentifier> Defined in Clause 5.4.3 Optional

function _updateTransitionTime(node) {
    // TransitionTime specifies the time when the current state was entered.
    if (node.transitionTime) {
        node.transitionTime.setValueFromSource({dataType: DataType.DateTime, value: (new Date())})
    }
}

function _updateEffectiveTransitionTime(node,subStateNode) {
    if (node.effectiveTransitionTime) {
        //xx console.log("xxxx _updateEffectiveTransitionTime because subStateNode ",subStateNode.browseName.toString());
        node.effectiveTransitionTime.setValueFromSource({ dataType: DataType.DateTime,value: (new Date())})
    }
}




function _getEffectiveDisplayName(node) {
    const dataValue = node.id.readValue();
    if (dataValue.statusCode !== StatusCodes.Good) {
        return dataValue;
    }
    assert(dataValue.value.dataType === DataType.Boolean);
    const boolValue = dataValue.value.value;

    const humanReadableString = _getHumanReadableString(node);

    let subStateNodes;
    if (boolValue) {
        subStateNodes = node.findReferencesExAsObject("HasTrueSubState",BrowseDirection.Forward);
    } else {
        subStateNodes = node.findReferencesExAsObject("HasFalseSubState",BrowseDirection.Forward);
    }
    const states = subStateNodes.forEach(function(n) {
        // todo happen
    });

    return humanReadableString;
}
function _getHumanReadableString(node) {

    let dataValue = node.id.readValue();
    if (dataValue.statusCode !== StatusCodes.Good) {
        return dataValue;
    }
    assert(dataValue.value.dataType === DataType.Boolean);
    const boolValue = dataValue.value.value;

    // The Value Attribute of a TwoStateVariable contains the current state as a human readable name.
    // The EnabledState for example, might contain the name “Enabled” when TRUE and “Disabled” when FALSE.

    let valueAsLocalizedText;

    if (boolValue) {
        const _trueState = (node._trueState) ? node._trueState: "TRUE";
        valueAsLocalizedText = { dataType: "LocalizedText", value: { text: _trueState}};

    } else {
        const _falseState = (node._falseState) ? node._falseState: "FALSE";
        valueAsLocalizedText = { dataType: "LocalizedText", value: { text: _falseState}};
    }
    dataValue = dataValue.clone();
    dataValue.value =new Variant(valueAsLocalizedText);
    return dataValue;

}


function _install_TwoStateVariable_machinery(node,options) {

    assert(node.dataTypeObj.browseName.toString()=== "LocalizedText");
    assert(node.minimumSamplingInterval === 0);
    assert(node.typeDefinitionObj.browseName.toString() === "TwoStateVariableType");
    assert(node.dataTypeObj.browseName.toString() === "LocalizedText");
    assert(node.hasOwnProperty("valueRank") && (node.valueRank === -1 || node.valueRank === 0));
    assert(node.hasOwnProperty("id"));
    options = options || {};
    // promote node into a UATwoStateVariable
    Object.setPrototypeOf(node,UATwoStateVariable.prototype);
    node.initialize(options);
}


/***
 * @class UATwoStateVariable
 * @constructor
 * @extends UAVariable
 */
function UATwoStateVariable() {
    // UAVariable.apply(this,arguments);
}
util.inherits(UATwoStateVariable,UAVariable);

/**
 * @method initialize
 * @private
 * @param options
 */
UATwoStateVariable.prototype.initialize = function(options) {

    const node = this;

    if (options.trueState) {
        assert(options.falseState);
        assert(typeof(options.trueState)==="string" );
        assert(typeof(options.falseState)==="string" );
        node._trueState  = options.trueState;
        node._falseState = options.falseState;

        if (node.falseState) {
            node.falseState.bindVariable({
                get: function() {
                    const node = this;
                    return new Variant({
                        dataType: DataType.LocalizedText,
                        value: node._falseState
                    });
                }
            },true);

        }
        if (node.trueState) {
            node.trueState.bindVariable({
                get: function () {
                    const node = this;
                    return new Variant({
                        dataType: DataType.LocalizedText,
                        value: node._trueState
                    });
                }
            }, true);
        }
    }
    node.id.setValueFromSource( {dataType: "Boolean", value: false} , StatusCodes.UncertainInitialValue);

    // handle isTrueSubStateOf
    if (options.isTrueSubStateOf) {
        node.addReference({ referenceType: "HasTrueSubState", isForward: false, nodeId: options.isTrueSubStateOf});
    }

    if (options.isFalseSubStateOf) {
        node.addReference({ referenceType: "HasFalseSubState", isForward: false, nodeId: options.isFalseSubStateOf});
    }

    if(node.effectiveTransitionTime) {
        // install "value_changed" event handler on SubState that are already defined
        const subStates = [].concat(node.getTrueSubStates(),node.getFalseSubStates());
        for(let subState of subStates) {
            subState.on("value_changed",_updateEffectiveTransitionTime.bind(null,node,subState));
        }
    }

    // it should be possible to define a trueState and falseState LocalizedText even if the trueState or FalseState node
    // is not exposed. Therefore we need to store their value into dedicated variables.
    node.id.on("value_changed",function() {
        node._internal_set_dataValue(_getHumanReadableString(node));
    });
    node._internal_set_dataValue(_getHumanReadableString(node));

    // todo : also set the effectiveDisplayName if present

    // from spec Part 5
    // Release 1.03 OPC Unified Architecture, Part 5
    // EffectiveDisplayName contains a human readable name for the current state of the state
    // machine after taking the state of any SubStateMachines in account. There is no rule specified
    // for which state or sub-state should be used. It is up to the Server and will depend on the
    // semantics of the StateMachineType
    //
    // EffectiveDisplayName will be constructed by adding the EnableSdtate
    // and the State of the addTrue state
    if (node.effectiveDisplayName) {
        node.id.on("value_changed",function() {
            node.effectiveDisplayName._internal_set_dataValue(_getEffectiveDisplayName(node));
        });
        node.effectiveDisplayName._internal_set_dataValue(_getEffectiveDisplayName(node));
    }
};

const resolveNodeId = require("node-opcua-nodeid").resolveNodeId;
const sameNodeId = require("node-opcua-nodeid").sameNodeId;

const hasTrueSubState_ReferenceTypeNodeId = resolveNodeId("HasTrueSubState");
const hasFalseSubState_ReferenceTypeNodeId = resolveNodeId("HasFalseSubState");

// TODO : shall we care about overloading the remove_backward_reference method ?
// some TrueSubState and FalseSubState relationship may be added later
// so we need a mechanism to keep adding the "value_changed" event handle on subStates that
// will be defined later.
// install change detection on sub State
// this is useful to change the effective transitionTime
// EffectiveTransitionTime specifies the time when the current state or one of its sub states was entered.
// If, for example, a LevelAlarm is active and – while active – switches several times between High and
// HighHigh, then the TransitionTime stays at the point in time where the Alarm became active whereas the
// EffectiveTransitionTime changes with each shift of a sub state.
UATwoStateVariable.prototype._add_backward_reference = function(reference) {
    const self = this;
    const _base_add_backward_reference = UAVariable.prototype._add_backward_reference;
    // call base method
    _base_add_backward_reference.call(self,reference);

    if ( reference.isForward &&
         ( sameNodeId(reference.referenceType,hasTrueSubState_ReferenceTypeNodeId) ||
             sameNodeId(reference.referenceType,hasFalseSubState_ReferenceTypeNodeId)) ) {

        const addressSpace = self.addressSpace;
        // add event handle
        const subState = addressSpace.findNode(reference.nodeId);
        subState.on("value_changed",_updateEffectiveTransitionTime.bind(null,self,subState));
    }
};

/**
 * @method setValue
 * @param boolValue {Boolean}
 */
UATwoStateVariable.prototype.setValue = function TwoStateVariable_setValue(boolValue) {

    const node = this;
    assert(_.isBoolean(boolValue));
    const dataValue = node.id.readValue();
    const oldValue = dataValue.value.value;
    if (dataValue.statusCode === StatusCodes.Good && boolValue === oldValue) {
        return; // nothing to do
    }
    //
    node.id.setValueFromSource(new Variant({dataType: DataType.Boolean, value: boolValue}));
    _updateTransitionTime(node);
    _updateEffectiveTransitionTime(node,node);
};

/**
 * @method getValue
 * @return {Boolean}
 */
UATwoStateVariable.prototype.getValue = function TwoStateVariable_getValue() {

    const node = this;
    const dataValue = node.id.readValue();
    assert(dataValue.statusCode === StatusCodes.Good);
    assert(dataValue.value.dataType === DataType.Boolean);
    return dataValue.value.value;
};
/**
 * @method getValueAsString
 * @return {string}
 */
UATwoStateVariable.prototype.getValueAsString = function TwoStateVariable_getValue() {
    const node = this;
    const dataValue = node.readValue();
    assert(dataValue.statusCode === StatusCodes.Good);
    assert(dataValue.value.dataType === DataType.LocalizedText);
    return dataValue.value.value.text.toString();

};
exports.UATwoStateVariable = UATwoStateVariable;

exports.install = function (AddressSpace) {

    assert(_.isUndefined(AddressSpace._install_TwoStateVariable_machinery ));
    AddressSpace._install_TwoStateVariable_machinery = _install_TwoStateVariable_machinery;

    /**
     *
     * @method addTwoStateVariable
     *
     * @param options
     * @param options.browseName  {String}
     * @param [options.description {String}]
     * @param [options.modellingRule {String}]
     * @param [options.minimumSamplingInterval {Number} =0]
     * @param options.componentOf {Node|NodeId}
     * @param options.propertyOf {Node|NodeId}
     * @param options.trueState {String}
     * @param options.falseState {String}
     * @param [options.isTrueSubStateOf {NodeId}]
     * @param [options.isFalseSubStateOf {NodeId}]
     * @param [options.modellingRule]
     * @return {UATwoStateVariable}
     *
     * Optionals can be EffectiveDisplayName, TransitionTime, EffectiveTransitionTime
     */
    AddressSpace.prototype.addTwoStateVariable   = function (options) {

        return this._resolveRequestedNamespace(options).addTwoStateVariable(options);

    };
    Namespace.prototype.addTwoStateVariable = function(options){

        const namespace = this;
        assert(options.browseName," a browseName is required");
        const addressSpace = namespace.addressSpace;

        const twoStateVariableType = addressSpace.findVariableType("TwoStateVariableType");

        options.optionals = options.optionals || [];
        if (options.trueState) {
            options.optionals.push("TrueState");
        }
        if (options.falseState) {
            options.optionals.push("FalseState");
        }

        // we want event based change...
        options.minimumSamplingInterval = 0;

        const node = twoStateVariableType.instantiate({
            browseName: options.browseName,

            nodeId: options.nodeId,

            description: options.description,

            organizedBy: options.organizedBy,
            componentOf: options.componentOf,

            modellingRule: options.modellingRule,

            minimumSamplingInterval: options.minimumSamplingInterval,
            optionals: options.optionals
        });

        _install_TwoStateVariable_machinery(node,options);

        return node;
    };
};