예제 #1
0
    def do_function(self, line):
        """Send a function test after validating the function test (as JSON)."""
        point_defs = PointDefinitions(
            point_definitions_path=POINT_DEFINITIONS_PATH)
        ftest = FunctionTest(FUNCTION_DEFINITIONS_PATH, CURVE_JSON)
        ftest.is_valid()
        for func_step_def in ftest.get_function_def().steps:
            try:
                point_value = ftest.points[func_step_def.name]
            except KeyError:
                continue
            pdef = point_defs.point_named(func_step_def.name)
            if not pdef:
                raise ValueError("Point definition not found: {}".format(
                    pdef.name))

            if type(point_value) == list:
                self.application.send_array(point_value, pdef)
            else:
                try:
                    send_func = self.application.SEND_FUNCTIONS[
                        func_step_def.fcodes[0] if func_step_def.
                        fcodes else DIRECT_OPERATE]
                except (KeyError, IndexError):
                    raise ValueError("Unrecognized sent command function")

                self.application.send_command(send_func, pdef, point_value)
예제 #2
0
    def do_function(self, line):
        """Send a function test after validating the function test (as JSON)."""
        point_defs = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH)
        ftest = FunctionTest(FUNCTION_DEFINITIONS_PATH, CURVE_JSON)
        ftest.is_valid()
        for func_step_def in ftest.get_function_def().steps:
            try:
                point_value = ftest.points[func_step_def.name]
            except KeyError:
                continue
            pdef = point_defs.point_named(func_step_def.name)
            if not pdef:
                raise ValueError("Point definition not found: {}".format(pdef.name))

            if type(point_value) == list:
                self.application.send_array(point_value, pdef)
            else:
                try:
                    send_func = self.application.SEND_FUNCTIONS[func_step_def.fcodes[0]
                    if func_step_def.fcodes
                    else DIRECT_OPERATE]
                except (KeyError, IndexError):
                    raise ValueError("Unrecognized sent command function")

                self.application.send_command(send_func, pdef, point_value)
예제 #3
0
    def send_function_test(self, point_def_path='', func_def_path='', func_test_path='', func_test_json=None):
        """
            Send a function test after validating the function test (as JSON).

        :param point_def_path: path to point definition config
        :param func_def_path: path to function definition config
        :param func_test_path: path to function test json
        :param func_test_json: function test json
        """
        ftest = FunctionTest(func_test_path, func_test_json, point_def_path=point_def_path, func_def_path=func_def_path)

        ftest.is_valid()

        pdefs = PointDefinitions(point_definitions_path=point_def_path or POINT_DEF_PATH)

        func_def = ftest.get_function_def()
        for func_step_def in func_def.steps:
            try:
                point_value = ftest.points[func_step_def.name]
            except KeyError:
                continue

            pdef = pdefs.point_named(func_step_def.name)  # No need to test for valid point name, as that was done above
            if not pdef:
                raise MesaMasterException("Point definition not found: {}".format(func_step_def.name))

            if type(point_value) == list:
                self.send_array(point_value, pdef)
            else:
                try:
                    send_func = self.SEND_FUNCTIONS[func_step_def.fcodes[0] if func_step_def.fcodes else DIRECT_OPERATE]
                except (KeyError, IndexError):
                    raise MesaMasterException("Unrecognized sent command function")

                self.send_command(send_func, pdef, point_value)
def test_function_definitions():
    point_definitions = PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH)
    fdefs = FunctionDefinitions(point_definitions, function_definitions_path=FUNCTION_DEFINITIONS_PATH)

    fd = fdefs.function_for_id("curve")
    print(fd)

    pdef = point_definitions.get_point_named("DCHD.WinTms (out)")
    print(pdef)
    print(fdefs.step_definition_for_point(pdef))
def test_function_definitions():
    point_definitions = PointDefinitions(
        point_definitions_path=POINT_DEFINITIONS_PATH)
    fdefs = FunctionDefinitions(
        point_definitions, function_definitions_path=FUNCTION_DEFINITIONS_PATH)

    fd = fdefs.function_for_id("curve")
    print(fd)

    pdef = point_definitions.get_point_named("DCHD.WinTms (out)")
    print(pdef)
    print(fdefs.step_definition_for_point(pdef))
예제 #6
0
 def __init__(self, func_test_path='', func_test_json=None, func_def_path='', point_def_path=''):
     self.func_def_path = func_def_path or FUNCTION_DEF_PATH
     self.point_definitions = PointDefinitions(point_definitions_path=point_def_path or POINT_DEF_PATH)
     self.ftest = func_test_json or json.load(open(func_test_path))
     self.function_id = self.ftest.get('function_id', self.ftest.get('id', None))
     self.function_name = self.ftest.get('function_name', self.ftest.get('name', None))
     self.name = self.ftest.get('name', None)
     self.points = {k: v for k, v in self.ftest.items() if k not in ["name", "function_id", "function_name", "id"]}
def test_point_definition_load():
    point_defs = PointDefinitions(
        point_definitions_path=POINT_DEFINITIONS_PATH)
    import pprint
    pprint.pprint(point_defs._points)
    pprint.pprint(point_defs._point_name_dict)
    print("_point_variations_dict")
    pprint.pprint(point_defs._point_variation_dict)
예제 #8
0
 def __init__(self, map_file):
     self.c_ao = 0
     self.c_do = 0
     self.c_ai = 0
     self.c_di = 0
     self.measurements = dict()
     self.out_json = list()
     self.file_dict = map_file
     self.processor_point_def = PointDefinitions()
     self.outstation = DNP3Outstation('', 0, '')
예제 #9
0
 def __init__(self,
              func_test_path='',
              func_test_json=None,
              func_def_path='',
              point_def_path=''):
     self.func_def_path = func_def_path or FUNCTION_DEF_PATH
     self.point_definitions = PointDefinitions(
         point_definitions_path=point_def_path or POINT_DEF_PATH)
     self.ftest = func_test_json or json.load(open(func_test_path))
     self.function_id = self.ftest.get('function_id',
                                       self.ftest.get('id', None))
     self.function_name = self.ftest.get('function_name',
                                         self.ftest.get('name', None))
     self.name = self.ftest.get('name', None)
     self.points = {
         k: v
         for k, v in self.ftest.items()
         if k not in ["name", "function_id", "function_name", "id"]
     }
예제 #10
0
    def load_point_definitions(self):
        """
            Load and cache a dictionary of PointDefinitions from a json list.

            Index the dictionary by point_type and point index.
        """
        _log.debug('Loading DNP3 point definitions.')
        try:
            self.point_definitions = PointDefinitions()
            self.point_definitions.load_points(self.points)
        except (AttributeError, TypeError) as err:
            if self._local_point_definitions_path:
                _log.warning(
                    "Attempting to load point definitions from local path.")
                self.point_definitions = PointDefinitions(
                    point_definitions_path=self._local_point_definitions_path)
            else:
                raise DNP3Exception(
                    "Failed to load point definitions from config store: {}".
                    format(err))
예제 #11
0
def load_and_validate_definitions():
    """
        Standalone method, intended to be invoked from the command line.

        Load PointDefinition and FunctionDefinition files as specified in command line args,
        and validate their contents.
    """
    # Grab JSON and YAML definition file paths from the command line.
    parser = argparse.ArgumentParser()
    parser.add_argument('point_defs', help='path of the point definitions file (json)')
    parser.add_argument('function_defs', help='path of the function definitions file (yaml)')
    args = parser.parse_args()

    point_definitions = PointDefinitions(point_definitions_path=args.point_defs)
    function_definitions = FunctionDefinitions(point_definitions, function_definitions_path=args.function_defs)
    validate_definitions(point_definitions, function_definitions)
예제 #12
0
 def test_load_points_from_json_file(self):
     try:
         PointDefinitions(point_definitions_path=POINT_DEFINITIONS_PATH)
         assert True
     except ValueError:
         assert False
예제 #13
0
class FunctionTest(object):

    def __init__(self, func_test_path='', func_test_json=None, func_def_path='', point_def_path=''):
        self.func_def_path = func_def_path or FUNCTION_DEF_PATH
        self.point_definitions = PointDefinitions(point_definitions_path=point_def_path or POINT_DEF_PATH)
        self.ftest = func_test_json or json.load(open(func_test_path))
        self.function_id = self.ftest.get('function_id', self.ftest.get('id', None))
        self.function_name = self.ftest.get('function_name', self.ftest.get('name', None))
        self.name = self.ftest.get('name', None)
        self.points = {k: v for k, v in self.ftest.items() if k not in ["name", "function_id", "function_name", "id"]}

    def get_function_def(self):
        """
            Gets the function definition for the function test. Returns None if no definition is found.
        """
        fdefs = FunctionDefinitions(point_definitions=self.point_definitions,
                                    function_definitions_path=self.func_def_path)
        return fdefs.function_for_id(self.function_id)

    @staticmethod
    def get_mandatory_steps(func_def):
        """
            Returns list of mandatory steps for the given function definition.

        :param func_def: function definition
        """
        return [step.name for step in func_def.steps if step.optional == 'M']

    def has_mandatory_steps(self, fdef=None):
        """
            Returns True if the instance has all required steps, and raises an exception if not.

        :param fdef: function definition
        """
        fdef = fdef or self.get_function_def()
        if not fdef:
            raise FunctionTestException("Function definition not found: {}".format(self.function_id))

        if not all(step in self.ftest.keys() for step in self.get_mandatory_steps(fdef)):
            raise FunctionTestException("Function Test missing mandatory steps")

        return True

    def points_resolve(self, func_def):
        """
            Returns true if all the points in the instance resolve to point names in the function definition,
            and raises an exception if not.

        :param func_def: function definition of the given instance
        """
        # It would have been more informative to identify the mismatched step/point name,
        # but that would break a pytest assertion that matches on this specific exception description.
        if not all(step_name in [step.point_def.name for step in func_def.steps] for step_name in self.points.keys()):
            raise FunctionTestException("Not all points resolve")
        return True

    def correct_point_types(self):
        """
            Check valid point value.
        """
        for point_name, point_value in self.points.items():
            point_def = self.point_definitions.point_named(point_name)
            point_values = sum([list(v.values()) for v in point_value], []) if point_def.is_array else [point_value]
            for value in point_values:
                if type(value) not in POINT_TYPE_TO_PYTHON_TYPE[POINT_TYPES_BY_GROUP[point_def.group]]:
                    # It would have been more informative to display the value and/or type in the error message,
                    # but that would break a pytest assertion that matches on this specific exception description.
                    raise FunctionTestException("Invalid point value: {}".format(point_name))
        return True

    def is_valid(self):
        """
            Returns True if the function test passes two validation steps:
                1. it has all the mandatory steps
                2. its point names resolve to point names in the function definition
                3. its point value is valid
            If the function test is invalid, an exception is raised.
        """
        f_def = self.get_function_def()

        try:
            self.has_mandatory_steps(f_def)
            self.points_resolve(f_def)
            self.correct_point_types()
            return True
        except Exception as err:
            raise FunctionTestException("Validation Error: {}".format(str(err)))
예제 #14
0
import copy

from dnp3.points import PointDefinitions
from dnp3.mesa.functions import FunctionDefinitions, FunctionDefinition, StepDefinition

from test_mesa_agent import POINT_DEFINITIONS_PATH, FUNCTION_DEFINITIONS_PATH

POINT_DEFINITIONS = PointDefinitions(
    point_definitions_path=POINT_DEFINITIONS_PATH)

enable_high_voltage_ride_through_mode = {
    'id':
    'enable_high_voltage_ride_through_mode',
    'name':
    'Enable High Volatge Ride-Through Mode',
    'ref':
    'AN2018 Spec section 2.5.1 Table 33',
    'steps': [{
        'step_number': 1,
        'description': 'Set the Reference Voltage if it is not already set',
        'point_name': 'DECP.VRef.AO0',
        'optional': 'I',
        'fcode': ['direct_operate'],
        'response': 'DECP.VRef.AI29'
    }, {
        'step_number': 2,
        'description':
        'Set the Reference Voltage Offset if it is not already set',
        'point_name': 'DECP.VRefOfs.AO1',
        'optional': 'I',
        'fcode': ['direct_operate'],
예제 #15
0
class BaseDNP3Agent(Agent):
    """
        DNP3Agent is a VOLTTRON agent that handles DNP3 outstation communications.

        DNP3Agent models a DNP3 outstation, communicating with a DNP3 master.

        For further information about this agent and DNP3 communications, please see the VOLTTRON
        DNP3 specification, located in VOLTTRON readthedocs
        under http://volttron.readthedocs.io/en/develop/specifications/dnp3_agent.html.

        This agent can be installed from a command-line shell as follows:
            export VOLTTRON_ROOT=<your volttron install directory>
            export DNP3_ROOT=$VOLTTRON_ROOT/services/core/DNP3Agent
            cd $VOLTTRON_ROOT
            python scripts/install-agent.py -s $DNP3_ROOT -i dnp3agent -c $DNP3_ROOT/dnp3agent.config -t dnp3agent -f
    """
    def __init__(self,
                 points=None,
                 point_topic='',
                 local_ip=None,
                 port=None,
                 outstation_config=None,
                 local_point_definitions_path=None,
                 **kwargs):
        """Initialize the DNP3 agent."""
        super(BaseDNP3Agent, self).__init__(**kwargs)
        self.points = points
        self.point_topic = point_topic
        self.local_ip = local_ip
        self.port = port
        self.outstation_config = outstation_config
        self.default_config = {
            'points': points,
            'point_topic': point_topic,
            'local_ip': local_ip,
            'port': port,
            'outstation_config': outstation_config,
        }
        self.application = None
        self.volttron_points = None

        self.point_definitions = None
        self._current_point_values = {}
        self._current_array = None
        self._local_point_definitions_path = local_point_definitions_path

        self.vip.config.set_default('config', self.default_config)
        self.vip.config.subscribe(self._configure,
                                  actions=['NEW', 'UPDATE'],
                                  pattern='config')

    def _configure(self, config_name, action, contents):
        """Initialize/Update the agent configuration."""
        self._configure_parameters(contents)

    def load_point_definitions(self):
        """
            Load and cache a dictionary of PointDefinitions from a json list.

            Index the dictionary by point_type and point index.
        """
        _log.debug('Loading DNP3 point definitions.')
        try:
            self.point_definitions = PointDefinitions()
            self.point_definitions.load_points(self.points)
        except (AttributeError, TypeError) as err:
            if self._local_point_definitions_path:
                _log.warning(
                    "Attempting to load point definitions from local path.")
                self.point_definitions = PointDefinitions(
                    point_definitions_path=self._local_point_definitions_path)
            else:
                raise DNP3Exception(
                    "Failed to load point definitions from config store: {}".
                    format(err))

    def start_outstation(self):
        """Start the DNP3Outstation instance, kicking off communication with the DNP3 Master."""
        _log.info('Starting DNP3Outstation')
        self.publish_outstation_status('starting')
        self.application = DNP3Outstation(self.local_ip, self.port,
                                          self.outstation_config)
        self.application.start()
        self.publish_outstation_status('running')

    def stop_outstation(self):
        """Shutdown the DNP3Outstation application."""
        _log.info('Stopping DNP3Outstation')
        self.publish_outstation_status('stopping')
        self.application.shutdown()
        self.publish_outstation_status('stopped')
        self.application = None

    def _configure_parameters(self, contents):
        """
            Initialize/Update the DNP3 agent configuration.

            DNP3Agent configuration parameters (the MesaAgent subclass has some more):

            points: (string) A JSON structure of point definitions to be loaded.
            point_topic: (string) Message bus topic to use when publishing DNP3 point values.
                        Default: mesa/point.
            outstation_status_topic: (string) Message bus topic to use when publishing outstation status.
                        Default: mesa/outstation_status.
            local_ip: (string) Outstation's host address (DNS resolved).
                        Default: 0.0.0.0.
            port: (integer) Outstation's port number - the port that the remote endpoint (Master) is listening on.
                        Default: 20000.
            outstation_config: (dictionary) Outstation configuration parameters. All are optional.
                Parameters include:
                    database_sizes: (integer) Size of each DNP3 database buffer.
                                Default: 10000.
                    event_buffers: (integer) Size of the database event buffers.
                                Default: 10.
                    allow_unsolicited: (boolean) Whether to allow unsolicited requests.
                                Default: True.
                    link_local_addr: (integer) Link layer local address.
                                Default: 10.
                    link_remote_addr: (integer) Link layer remote address.
                                Default: 1.
                    log_levels: List of bit field names (OR'd together) that filter what gets logged by DNP3.
                                Default: [NORMAL].
                                Possible values: ALL, ALL_APP_COMMS, ALL_COMMS, NORMAL, NOTHING.
                    threads_to_allocate: (integer) Threads to allocate in the manager's thread pool.
                                Default: 1.
        """
        config = self.default_config.copy()
        config.update(contents)
        self.points = config.get('points', [])
        self.point_topic = config.get('point_topic', DEFAULT_POINT_TOPIC)
        self.outstation_status_topic = config.get(
            'outstation_status_topic', DEFAULT_OUTSTATION_STATUS_TOPIC)
        self.local_ip = config.get('local_ip', DEFAULT_LOCAL_IP)
        self.port = int(config.get('port', DEFAULT_PORT))
        self.outstation_config = config.get('outstation_config', {})
        _log.debug('DNP3Agent configuration parameters:')
        _log.debug('\tpoints type={}'.format(type(self.points)))
        _log.debug('\tpoint_topic={}'.format(self.point_topic))
        _log.debug('\toutstation_status_topic={}'.format(
            self.outstation_status_topic))
        _log.debug('\tlocal_ip={}'.format(self.local_ip))
        _log.debug('\tport={}'.format(self.port))
        _log.debug('\toutstation_config={}'.format(self.outstation_config))
        self.load_point_definitions()
        DNP3Outstation.set_agent(self)

        # Stop outstation if DNP3 config has been changed
        if self.application and (
                self.application.local_ip, self.application.port,
                self.application.outstation_config) != (
                    self.local_ip, self.port, self.outstation_config):
            self.stop_outstation()

        # Start outstation if the DNP3 application has not started
        if not self.application:
            self.start_outstation()

        return config

    @RPC.export
    def reset(self):
        """Reset the agent's internal state, emptying point value caches. Used during iterative testing."""
        _log.info('Resetting agent state.')
        self._current_point_values = {}
        self._current_array = {}

    def get_current_point_value(self, data_type, index):
        """Return the most-recently-received PointValue for a given PointDefinition."""
        if data_type not in self._current_point_values or index not in self._current_point_values[
                data_type]:
            return None
        else:
            return self._current_point_values[data_type][index]

    def _set_point(self, point_name, value):
        """
            (Internal) Set the value of a given input point (no debug trace).

        @param point_name: The VOLTTRON point name of a DNP3 PointDefinition.
        @param value: The value to set. The value's data type must match the one in the DNP3 PointDefinition.
        """
        point_properties = self.volttron_points.get(point_name, {})
        data_type = point_properties.get('data_type', None)
        index = point_properties.get('index', None)
        try:
            if data_type == DATA_TYPE_ANALOG_INPUT:
                wrapped_value = opendnp3.Analog(value)
            elif data_type == DATA_TYPE_BINARY_INPUT:
                wrapped_value = opendnp3.Binary(value)
            else:
                raise Exception(
                    'Unexpected data type for DNP3 point named {0}'.format(
                        point_name))
            DNP3Outstation.apply_update(wrapped_value, index)
        except Exception as e:
            raise DNP3Exception(e)

    def process_point_value(self, command_type, command, index, op_type):
        """
            A point value was received from the Master. Process its payload.

        @param command_type: Either 'Select' or 'Operate'.
        @param command: A ControlRelayOutputBlock or else a wrapped data value (AnalogOutputInt16, etc.).
        @param index: DNP3 index of the payload's data definition.
        @param op_type: An OperateType, or None if command_type == 'Select'.
        @return: A CommandStatus value.
        """
        try:
            point_value = self.point_definitions.point_value_for_command(
                command_type, command, index, op_type)
            if point_value is None:
                return opendnp3.CommandStatus.DOWNSTREAM_FAIL
        except Exception as ex:
            _log.error(
                'No DNP3 PointDefinition for command with index {}'.format(
                    index))
            return opendnp3.CommandStatus.DOWNSTREAM_FAIL

        try:
            self._process_point_value(point_value)
        except Exception as ex:
            _log.error('Error processing DNP3 command: {}'.format(ex))
            # Delete a cached point value (typically occurs only if an error is being handled).
            try:
                self._current_point_values.get(point_value.point_def.data_type,
                                               {}).pop(int(point_value.index),
                                                       None)
            except Exception as err:
                _log.error(
                    'Error discarding cached value {}'.format(point_value))
            return opendnp3.CommandStatus.DOWNSTREAM_FAIL

        return opendnp3.CommandStatus.SUCCESS

    def _process_point_value(self, point_value):
        _log.info('Received DNP3 {}'.format(point_value))
        if point_value.command_type == 'Select':
            # Perform any needed validation now, then wait for the subsequent Operate command.
            return None
        else:
            # Update a dictionary that holds the most-recently-received value of each point.
            self._current_point_values.setdefault(
                point_value.point_def.data_type,
                {})[int(point_value.index)] = point_value
            return point_value

    def get_point_named(self, point_name):
        return self.point_definitions.get_point_named(point_name)

    def update_array_for_point(self, point_value):
        """A received point belongs to a PointArray. Update it."""
        if point_value.point_def.is_array_head_point:
            self._current_array = PointArray(point_value.point_def)
        elif self._current_array is None:
            raise DNP3Exception(
                'Array point received, but there is no current Array.')
        elif not self._current_array.contains_index(point_value.index):
            raise DNP3Exception(
                'Received Array point outside of current Array.')
        self._current_array.add_point_value(point_value)

    def update_input_point(self, point_def, value):
        """
            Update an input point. This may send its PointValue to the Master.

        :param point_def: A PointDefinition.
        :param value: A value to send (unwrapped simple data type, or else a list/array).
        """
        if type(value) == list:
            # It's an array. Break it down into its constituent points, and apply each one separately.
            col_count = len(point_def.array_points)
            cols_by_name = {
                pt['name']: col
                for col, pt in enumerate(point_def.array_points)
            }
            for row_number, point_dict in enumerate(value):
                for pt_name, pt_val in point_dict.items():
                    pt_index = point_def.index + col_count * row_number + cols_by_name[
                        pt_name]
                    array_point_def = self.point_definitions.get_point_named(
                        point_def.name, index=pt_index)
                    self._apply_point_update(array_point_def, pt_index, pt_val)
        else:
            self._apply_point_update(point_def, point_def.index, value)

    @staticmethod
    def _apply_point_update(point_def, point_index, value):
        """
            Set an input point in the outstation database. This may send its PointValue to the Master.

        :param point_def: A PointDefinition.
        :param point_index: A numeric index for the point.
        :param value: A value to send (unwrapped, simple data type).
        """
        data_type = point_def.data_type
        if data_type == DATA_TYPE_ANALOG_INPUT:
            wrapped_val = opendnp3.Analog(float(value))
            if isinstance(value,
                          bool) or not isinstance(value, numbers.Number):
                # Invalid data type
                raise DNP3Exception('Received {} value for {}.'.format(
                    type(value), point_def))
        elif data_type == DATA_TYPE_BINARY_INPUT:
            wrapped_val = opendnp3.Binary(value)
            if not isinstance(value, bool):
                # Invalid data type
                raise DNP3Exception('Received {} value for {}.'.format(
                    type(value), point_def))
        else:
            # The agent supports only DNP3's Analog and Binary point types at this time.
            raise DNP3Exception('Unsupported point type {}'.format(data_type))
        if wrapped_val is not None:
            DNP3Outstation.apply_update(wrapped_val, point_index)
        _log.debug('Sent DNP3 point {}, value={}'.format(
            point_def, wrapped_val.value))

    def publish_point_value(self, point_value):
        """Publish a PointValue as it is received from the DNP3 Master."""
        _log.info('Publishing DNP3 {}'.format(point_value))
        msg = {
            point_value.name:
            (point_value.unwrapped_value() if point_value else None)
        }

        if point_value.point_def.action == PUBLISH_AND_RESPOND:
            msg.update({'response': point_value.point_def.response})

        self.publish_data(self.point_topic, msg)

    def publish_outstation_status(self, outstation_status):
        """Publish outstation status."""
        _log.info('Publishing outstation status: {}'.format(outstation_status))
        self.publish_data(self.outstation_status_topic, outstation_status)

    def publish_data(self, topic, msg):
        """Publish a payload to the message bus."""
        try:
            self.vip.pubsub.publish(peer='pubsub',
                                    topic=topic,
                                    headers={
                                        headers.TIMESTAMP:
                                        utils.format_timestamp(
                                            utils.get_aware_utc_now())
                                    },
                                    message=msg)
        except Exception as err:
            if os.environ.get('UNITTEST', False):
                _log.debug(
                    'Disregarding publish_data exception during unit test')
            else:
                raise DNP3Exception(
                    'Error publishing topic {}, message {}: {}'.format(
                        topic, msg, err))

    def dnp3_point_name(self, point_name):
        """
            Return a point's DNP3 point name, mapped from its VOLTTRON point name if necessary.

            If VOLTTRON point names were configured (by the DNP device driver), map them to DNP3 point names.
        """
        dnp3_point_name = self.volttron_points.get(
            point_name, '') if self.volttron_points else point_name
        if not dnp3_point_name:
            raise DNP3Exception(
                'No configured point for {}'.format(point_name))
        return dnp3_point_name

    @RPC.export
    def get_point(self, point_name):
        """
            Look up the most-recently-received value for a given output point.

        @param point_name: The point name of a DNP3 PointDefinition.
        @return: The (unwrapped) value of a received point.
        """
        _log.info('Getting point value for {}'.format(point_name))
        try:
            point_name = self.dnp3_point_name(point_name)
            point_def = self.point_definitions.get_point_named(point_name)
            point_value = self.get_current_point_value(point_def.data_type,
                                                       point_def.index)
            return point_value.unwrapped_value() if point_value else None
        except Exception as e:
            raise DNP3Exception(e)

    @RPC.export
    def get_point_by_index(self, data_type, index):
        """
            Look up the most-recently-received value for a given point.

        @param data_type: The data_type of a DNP3 point.
        @param index: The index of a DNP3 point.
        @return: The (unwrapped) value of a received point.
        """
        _log.info('Getting point value for data_type {} and index {}'.format(
            data_type, index))
        try:
            point_value = self.get_current_point_value(data_type, index)
            return point_value.unwrapped_value() if point_value else None
        except Exception as e:
            raise DNP3Exception(e)

    @RPC.export
    def get_points(self, point_list):
        """
            Look up the most-recently-received value of each configured output point.

        @param point_list: A list of point names.
        @return: A dictionary of point values, indexed by their point names.
        """
        _log.info(
            'Getting values for the following points: {}'.format(point_list))
        try:
            return {name: self.get_point(name) for name in point_list}
        except Exception as e:
            raise DNP3Exception(e)

    @RPC.export
    def get_configured_points(self):
        """
            Look up the most-recently-received value of each configured point.

        @return: A dictionary of point values, indexed by their point names.
        """
        if self.volttron_points is None:
            raise DNP3Exception('DNP3 points have not been configured')

        _log.info('Getting all DNP3 configured point values')
        try:
            return {
                name: self.get_point(name)
                for name in self.volttron_points
            }
        except Exception as e:
            raise DNP3Exception(e)

    @RPC.export
    def set_point(self, point_name, value):
        """
            Set the value of a given input point.

        @param point_name: The point name of a DNP3 PointDefinition.
        @param value: The value to set. The value's data type must match the one in the DNP3 PointDefinition.
        """
        _log.info('Setting DNP3 {} point value = {}'.format(point_name, value))
        try:
            self.update_input_point(
                self.get_point_named(self.dnp3_point_name(point_name)), value)

        except Exception as e:
            raise DNP3Exception(e)

    @RPC.export
    def set_points(self, point_dict):
        """
            Set point values for a dictionary of points.

        @param point_dict: A dictionary of {point_name: value} for a list of DNP3 points to set.
        """
        _log.info('Setting DNP3 point values: {}'.format(point_dict))
        try:
            for point_name, value in point_dict.items():
                self.update_input_point(
                    self.get_point_named(self.dnp3_point_name(point_name)),
                    value)
        except Exception as e:
            raise DNP3Exception(e)

    @RPC.export
    def config_points(self, point_map):
        """
            For each of the agent's points, map its VOLTTRON point name to its DNP3 group and index.

        @param point_map: A dictionary that maps a point's VOLTTRON point name to its DNP3 group and index.
        """
        _log.info('Configuring DNP3 points: {}'.format(point_map))
        self.volttron_points = point_map

    @RPC.export
    def get_point_definitions(self, point_name_list):
        """
            For each DNP3 point name in point_name_list, return a dictionary with each of the point definitions.

            The returned dictionary looks like this:

            {
                "point_name1": {
                    "property1": "property1_value",
                    "property2": "property2_value",
                    ...
                },
                "point_name2": {
                    "property1": "property1_value",
                    "property2": "property2_value",
                    ...
                }
            }

            If a definition cannot be found for a point name, it is omitted from the returned dictionary.

        :param point_name_list: A list of point names.
        :return: A dictionary of point definitions.
        """
        _log.info('Fetching a list of DNP3 point definitions for {}'.format(
            point_name_list))
        try:
            response = {}
            for name in point_name_list:
                point_def = self.point_definitions.get_point_named(
                    self.dnp3_point_name(name))
                if point_def is not None:
                    response[name] = point_def.as_json()
            return response
        except Exception as e:
            raise DNP3Exception(e)
예제 #16
0
class FunctionTest(object):
    def __init__(self,
                 func_test_path='',
                 func_test_json=None,
                 func_def_path='',
                 point_def_path=''):
        self.func_def_path = func_def_path or FUNCTION_DEF_PATH
        self.point_definitions = PointDefinitions(
            point_definitions_path=point_def_path or POINT_DEF_PATH)
        self.ftest = func_test_json or json.load(open(func_test_path))
        self.function_id = self.ftest.get('function_id',
                                          self.ftest.get('id', None))
        self.function_name = self.ftest.get('function_name',
                                            self.ftest.get('name', None))
        self.name = self.ftest.get('name', None)
        self.points = {
            k: v
            for k, v in self.ftest.items()
            if k not in ["name", "function_id", "function_name", "id"]
        }

    def get_function_def(self):
        """
            Gets the function definition for the function test. Returns None if no definition is found.
        """
        fdefs = FunctionDefinitions(
            point_definitions=self.point_definitions,
            function_definitions_path=self.func_def_path)
        return fdefs.function_for_id(self.function_id)

    @staticmethod
    def get_mandatory_steps(func_def):
        """
            Returns list of mandatory steps for the given function definition.

        :param func_def: function definition
        """
        return [step.name for step in func_def.steps if step.optional == 'M']

    def has_mandatory_steps(self, fdef=None):
        """
            Returns True if the instance has all required steps, and raises an exception if not.

        :param fdef: function definition
        """
        fdef = fdef or self.get_function_def()
        if not fdef:
            raise FunctionTestException(
                "Function definition not found: {}".format(self.function_id))

        if not all(step in self.ftest.keys()
                   for step in self.get_mandatory_steps(fdef)):
            raise FunctionTestException(
                "Function Test missing mandatory steps")

        return True

    def points_resolve(self, func_def):
        """
            Returns true if all the points in the instance resolve to point names in the function definition,
            and raises an exception if not.

        :param func_def: function definition of the given instance
        """
        # It would have been more informative to identify the mismatched step/point name,
        # but that would break a pytest assertion that matches on this specific exception description.
        if not all(
                step_name in [step.point_def.name for step in func_def.steps]
                for step_name in self.points.keys()):
            raise FunctionTestException("Not all points resolve")
        return True

    def correct_point_types(self):
        """
            Check valid point value.
        """
        for point_name, point_value in self.points.items():
            point_def = self.point_definitions.point_named(point_name)
            point_values = sum([list(v.values()) for v in point_value],
                               []) if point_def.is_array else [point_value]
            for value in point_values:
                if type(value) not in POINT_TYPE_TO_PYTHON_TYPE[
                        POINT_TYPES_BY_GROUP[point_def.group]]:
                    # It would have been more informative to display the value and/or type in the error message,
                    # but that would break a pytest assertion that matches on this specific exception description.
                    raise FunctionTestException(
                        "Invalid point value: {}".format(point_name))
        return True

    def is_valid(self):
        """
            Returns True if the function test passes two validation steps:
                1. it has all the mandatory steps
                2. its point names resolve to point names in the function definition
                3. its point value is valid
            If the function test is invalid, an exception is raised.
        """
        f_def = self.get_function_def()

        try:
            self.has_mandatory_steps(f_def)
            self.points_resolve(f_def)
            self.correct_point_types()
            return True
        except Exception as err:
            raise FunctionTestException("Validation Error: {}".format(
                str(err)))