def send_point(self, pdefs, func_def_path, point_name, point_value, step_number): """ Send a point to outstation. Check for validation. :param pdefs: point definitions :param func_def_path: path to function definition :param point_name: name of the point that will be sent :param point_value: value of the point that will be sent :param step_number: step number of the point that will be sent """ pdef = pdefs.point_named(point_name) if not pdef: raise MesaMasterTestException("Point definition not found: {}".format(point_name)) if not pdef.point_type: raise MesaMasterTestException("Unrecognized point type: {}".format(pdef.point_type)) step_def = FunctionDefinitions(pdefs, func_def_path).step_definition_for_point(pdef) if step_number != step_def.step_number: raise MesaMasterTestException("Step not in order: {}".format(step_number)) if type(point_value) == list: self.send_array(point_value, pdef) else: fdefs = FunctionDefinitions(pdefs, function_definitions_path=func_def_path) step_def = fdefs.step_definition_for_point(pdef) send_func = self.SEND_FUNCTIONS.get(step_def.fcodes[0] if step_def.fcodes else DIRECT_OPERATE, None) if not send_func: raise MesaMasterTestException("Unrecognized function code") if pdef.point_type in POINT_TYPE_TO_PYTHON_TYPE and \ type(point_value) not in POINT_TYPE_TO_PYTHON_TYPE[pdef.point_type]: raise MesaMasterTestException("Invalid point value: {}".format(pdef.name)) self.send_command(send_func, pdef, point_value)
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)
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)
def validate_functions_definition(functions_json): exception = {} try: function_definitions = FunctionDefinitions(POINT_DEFINITIONS) function_definitions.load_functions(functions_json) except Exception as err: exception['key'] = type(err).__name__ exception['error'] = str(err) return exception
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))
def load_function_definitions(self): """Populate the FunctionDefinitions repository from JSON in the config store.""" _log.debug('Loading MESA function definitions') try: self.function_definitions = FunctionDefinitions(self.point_definitions) self.function_definitions.load_functions(self.functions['functions']) except (AttributeError, TypeError) as err: if self._local_function_definitions_path: _log.warning("Attempting to load Function Definitions from local path.") self.function_definitions = FunctionDefinitions( self.point_definitions, function_definitions_path=self._local_function_definitions_path) else: raise DNP3Exception("Failed to load Function Definitions from config store: {}".format(err))
def load_function_definitions(self): """Populate the FunctionDefinitions repository from JSON in the config store.""" _log.debug('Loading MESA function definitions') try: if type(self.functions) == str: function_defs = self.get_from_config_store(self.functions) else: function_defs = self.functions self.function_definitions = FunctionDefinitions(self.point_definitions) self.function_definitions.load_functions(function_defs['functions']) except (AttributeError, TypeError) as err: if self._local_function_definitions_path: _log.warning("Attempting to load Function Definitions from local path.") self.function_definitions = FunctionDefinitions( self.point_definitions, function_definitions_path=self._local_function_definitions_path) else: raise DNP3Exception("Failed to load Function Definitions from config store: {}".format(err))
def send_point(self, pdefs, func_def_path, point_name, point_value, step_number): """ Send a point to outstation. Check for validation. :param pdefs: point definitions :param func_def_path: path to function definition :param point_name: name of the point that will be sent :param point_value: value of the point that will be sent :param step_number: step number of the point that will be sent """ pdef = pdefs.point_named(point_name) if not pdef: raise MesaMasterTestException( "Point definition not found: {}".format(point_name)) if not pdef.point_type: raise MesaMasterTestException("Unrecognized point type: {}".format( pdef.point_type)) step_def = FunctionDefinitions( pdefs, func_def_path).step_definition_for_point(pdef) if step_number != step_def.step_number: raise MesaMasterTestException( "Step not in order: {}".format(step_number)) if type(point_value) == list: self.send_array(point_value, pdef) else: fdefs = FunctionDefinitions( pdefs, function_definitions_path=func_def_path) step_def = fdefs.step_definition_for_point(pdef) send_func = self.SEND_FUNCTIONS.get( step_def.fcodes[0] if step_def.fcodes else DIRECT_OPERATE, None) if not send_func: raise MesaMasterTestException("Unrecognized function code") if pdef.point_type in POINT_TYPE_TO_PYTHON_TYPE and \ type(point_value) not in POINT_TYPE_TO_PYTHON_TYPE[pdef.point_type]: raise MesaMasterTestException("Invalid point value: {}".format( pdef.name)) self.send_command(send_func, pdef, point_value)
def send_json(self, pdefs, func_def_path, send_json_path='', send_json=None): """ Send a json in order for testing purpose. :param pdefs: point definitions :param func_def_path: path to function definition :param send_json_path: path to json that will be sent to the outstation :param send_json: json that will be sent to the outstation :return: """ if send_json_path: send_json = jsonapi.load(open(send_json_path), object_pairs_hook=OrderedDict) try: function_id = send_json['function_id'] except KeyError: raise MesaMasterTestException('Missing function_id') fdefs = FunctionDefinitions(pdefs, function_definitions_path=func_def_path) try: fdef = fdefs[function_id] except KeyError: raise MesaMasterTestException( 'Invalid function_id {}'.format(function_id)) step = 1 for name, value in send_json.items(): if name not in ['name', 'function_id', 'function_name']: pdef = pdefs.point_named(name) step_def = fdef[pdef] if step != step_def.step_number: raise MesaMasterTestException( "Step not in order: {}".format(step)) if type(value) == list: self.send_array(value, pdef) else: send_func = self.SEND_FUNCTIONS.get( step_def.fcodes[0] if step_def.fcodes else DIRECT_OPERATE, None) self.send_command(send_func, pdef, value) step += 1
class MesaAgent(BaseDNP3Agent): """ MesaAgent is a VOLTTRON agent that handles MESA-ESS DNP3 outstation communications. MesaAgent models a DNP3 outstation, communicating with a DNP3 master. For further information about this agent, MESA-ESS, and DNP3 communications, please see the VOLTTRON MESA-ESS agent specification, which can be found in VOLTTRON readthedocs at http://volttron.readthedocs.io/en/develop/specifications/mesa_agent.html. This agent can be installed from a command-line shell as follows: $ export VOLTTRON_ROOT=<volttron github install directory> $ cd $VOLTTRON_ROOT $ source services/core/DNP3Agent/install_mesa_agent.sh That file specifies a default agent configuration, which can be overridden as needed. """ def __init__(self, points=None, functions=None, point_topic='', local_ip=None, port=None, outstation_config=None, function_topic='', outstation_status_topic='', all_functions_supported_by_default='', local_function_definitions_path=None, **kwargs): """Initialize the MESA agent.""" super(MesaAgent, self).__init__(**kwargs) self.functions = functions self.function_topic = function_topic self.outstation_status_topic = outstation_status_topic self.all_functions_supported_by_default = all_functions_supported_by_default self.default_config = { 'points': points, 'functions': functions, 'point_topic': point_topic, 'local_ip': local_ip, 'port': port, 'outstation_config': outstation_config, 'function_topic': function_topic, 'outstation_status_topic': outstation_status_topic, 'all_functions_supported_by_default': all_functions_supported_by_default } self.vip.config.set_default('config', self.default_config) self.vip.config.subscribe(self._configure, actions=['NEW', 'UPDATE'], pattern='config') self.function_definitions = None self._current_func = None self._local_function_definitions_path = local_function_definitions_path def _configure_parameters(self, contents): """ Initialize/Update the MesaAgent configuration. See also the superclass version of this method, which does most of the initialization. MesaAgent configuration parameters: functions: (string) A JSON structure of function definitions to be loaded. function_topic: (string) Message bus topic to use when publishing MESA-ESS functions. Default: mesa/function. all_functions_supported_by_default: (boolean) When deciding whether to reject points for unsupported functions, ignore the values of their 'supported' points: simply treat all functions as supported. Default: False. """ config = super(MesaAgent, self)._configure_parameters(contents) self.functions = config.get('functions', {}) self.function_topic = config.get('function_topic', DEFAULT_FUNCTION_TOPIC) self.all_functions_supported_by_default = config.get('all_functions_supported_by_default', "False") _log.debug('MesaAgent configuration parameters:') _log.debug('\tfunctions type={}'.format(type(self.functions))) _log.debug('\tfunction_topic={}'.format(self.function_topic)) _log.debug('\tall_functions_supported_by_default={}'.format(self.all_functions_supported_by_default)) self.load_function_definitions() self.supported_functions = [] # Un-comment the next line to do more detailed validation and print definition statistics. # validate_definitions(self.point_definitions, self.function_definitions) @Core.receiver('onstart') def onstart(self, sender, **kwargs): """Start the DNP3Outstation instance, kicking off communication with the DNP3 Master.""" self._configure_parameters(self.default_config) _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 load_function_definitions(self): """Populate the FunctionDefinitions repository from JSON in the config store.""" _log.debug('Loading MESA function definitions') try: if type(self.functions) == str: function_defs = self.get_from_config_store(self.functions) else: function_defs = self.functions self.function_definitions = FunctionDefinitions(self.point_definitions) self.function_definitions.load_functions(function_defs['functions']) except (AttributeError, TypeError) as err: if self._local_function_definitions_path: _log.warning("Attempting to load Function Definitions from local path.") self.function_definitions = FunctionDefinitions( self.point_definitions, function_definitions_path=self._local_function_definitions_path) else: raise DNP3Exception("Failed to load Function Definitions from config store: {}".format(err)) @RPC.export def reset(self): """Reset the agent's internal state, emptying point value caches. Used during iterative testing.""" super(MesaAgent, self).reset() self.set_current_function(None) def _process_point_value(self, point_value): """ A PointValue was received from the Master. Process its payload. :param point_value: A PointValue. """ try: point_val = super(MesaAgent, self)._process_point_value(point_value) if point_val: self.update_function_for_point_value(point_val) # If we don't have a function, we don't care. if self.current_function: if self.current_function.has_input_point(): self.update_input_point( self.get_point_named(self.current_function.input_point_name()), point_val.unwrapped_value() ) if self.current_function.publish_now(): self.publish_function_step(self.current_function.last_step) except Exception as err: self.set_current_function(None) # Discard the current function raise DNP3Exception('Error processing point value: {}'.format(err)) def update_function_for_point_value(self, point_value): """Add point_value to the current Function if appropriate.""" try: current_function = self.current_function_for(point_value.point_def) if current_function is None: return None if point_value.point_def.is_array_point: self.update_array_for_point(point_value) current_function.add_point_value(point_value, current_array=self._current_array) except DNP3Exception as err: raise DNP3Exception('Error updating function: {}'.format(err)) def current_function_for(self, new_point_def): """A point was received. Return the current Function, updating it if necessary.""" new_point_function_def = self.function_definitions.get(new_point_def, None) if new_point_function_def is None: return None if self.current_function and new_point_function_def != self.current_function.definition: if not self.current_function.complete: raise DNP3Exception('Mismatch: {} does not belong to {}'.format(new_point_def, self.current_function)) # The current Function is done, and a new Function is arriving. Discard the old one. self.set_current_function(None) if not self.current_function: self.set_current_function(Function(new_point_function_def)) return self.current_function 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). """ super(MesaAgent, self).update_input_point(point_def, value) if type(value) != list: # Side-effect: If it's a Support point for a Function, update the Function's "supported" property. func = self.function_definitions.support_point_names().get(point_def.name, None) if func is not None and func.supported != value: _log.debug('Updating supported property to {} in {}'.format(value, func)) func.supported = value @property def current_function(self): """Return the Function being accumulated by the Outstation.""" return self._current_func def set_current_function(self, func): """Set the Function being accumulated by the Outstation to the supplied value, which might be None.""" if func: if self.all_functions_supported_by_default != "True": if not func.definition.supported: raise DNP3Exception('Received a point for unsupported {}'.format(func)) self._current_func = func return func def publish_function_step(self, step_to_send): """A Function Step was received from the DNP3 Master. Publish the Function.""" function_to_send = step_to_send.function msg = { "function_name": function_to_send.definition.name, "points": {step.definition.name: step.as_json(self.get_point_named(step.definition.name).type) for step in function_to_send.steps} } if step_to_send.definition.action == ACTION_PUBLISH_AND_RESPOND: msg["expected_response"] = step_to_send.definition.response _log.info('Publishing MESA {} message {}'.format(function_to_send, msg)) self.publish_data(self.function_topic, msg)
def test_load_functions_yaml(self): try: FunctionDefinitions(POINT_DEFINITIONS, FUNCTION_DEFINITIONS_PATH) assert True except ValueError: assert False
class MesaAgent(BaseDNP3Agent): """ MesaAgent is a VOLTTRON agent that handles MESA-ESS DNP3 outstation communications. MesaAgent models a DNP3 outstation, communicating with a DNP3 master. For further information about this agent, MESA-ESS, and DNP3 communications, please see the VOLTTRON MESA-ESS agent specification, which can be found in VOLTTRON readthedocs at http://volttron.readthedocs.io/en/develop/specifications/mesa_agent.html. This agent can be installed from a command-line shell as follows: $ export VOLTTRON_ROOT=<volttron github install directory> $ cd $VOLTTRON_ROOT $ source services/core/DNP3Agent/install_mesa_agent.sh That file specifies a default agent configuration, which can be overridden as needed. """ def __init__(self, functions=None, function_topic='', outstation_status_topic='', all_functions_supported_by_default=False, local_function_definitions_path=None, function_validation=False, **kwargs): """Initialize the MESA agent.""" super(MesaAgent, self).__init__(**kwargs) self.functions = functions self.function_topic = function_topic self.outstation_status_topic = outstation_status_topic self.all_functions_supported_by_default = all_functions_supported_by_default self.function_validation = function_validation # Update default config self.default_config.update({ 'functions': functions, 'function_topic': function_topic, 'outstation_status_topic': outstation_status_topic, 'all_functions_supported_by_default': all_functions_supported_by_default, 'function_validation': function_validation }) # Update default config in config store. self.vip.config.set_default('config', self.default_config) self.function_definitions = None self._local_function_definitions_path = local_function_definitions_path self._current_functions = dict() # {function_id: Function} self._current_block = dict() # {name: name, index: index} self._selector_block = dict() # {selector_block_point_name: {selector_index: [Step]}} self._edit_selectors = list() # [{name: name, index: index}] def _configure_parameters(self, contents): """ Initialize/Update the MesaAgent configuration. See also the superclass version of this method, which does most of the initialization. MesaAgent configuration parameters: functions: (string) A JSON structure of function definitions to be loaded. function_topic: (string) Message bus topic to use when publishing MESA-ESS functions. Default: mesa/function. all_functions_supported_by_default: (boolean) When deciding whether to reject points for unsupported functions, ignore the values of their 'supported' points: simply treat all functions as supported. Default: False. """ config = super(MesaAgent, self)._configure_parameters(contents) self.functions = config.get('functions', {}) self.function_topic = config.get('function_topic', DEFAULT_FUNCTION_TOPIC) self.all_functions_supported_by_default = config.get('all_functions_supported_by_default', False) self.function_validation = config.get('function_validation', False) _log.debug('MesaAgent configuration parameters:') _log.debug('\tfunctions type={}'.format(type(self.functions))) _log.debug('\tfunction_topic={}'.format(self.function_topic)) _log.debug('\tall_functions_supported_by_default={}'.format(bool(self.all_functions_supported_by_default))) _log.debug('\tfuntion_validation={}'.format(bool(self.function_validation))) self.load_function_definitions() self.supported_functions = [] # Un-comment the next line to do more detailed validation and print definition statistics. # validate_definitions(self.point_definitions, self.function_definitions) def load_function_definitions(self): """Populate the FunctionDefinitions repository from JSON in the config store.""" _log.debug('Loading MESA function definitions') try: self.function_definitions = FunctionDefinitions(self.point_definitions) self.function_definitions.load_functions(self.functions['functions']) except (AttributeError, TypeError) as err: if self._local_function_definitions_path: _log.warning("Attempting to load Function Definitions from local path.") self.function_definitions = FunctionDefinitions( self.point_definitions, function_definitions_path=self._local_function_definitions_path) else: raise DNP3Exception("Failed to load Function Definitions from config store: {}".format(err)) @RPC.export def reset(self): """Reset the agent's internal state, emptying point value caches. Used during iterative testing.""" super(MesaAgent, self).reset() self._current_functions = dict() self._current_block = dict() self._selector_block = dict() self._edit_selectors = list() @RPC.export def get_selector_block(self, block_name, index): try: return {step.definition.name: step.as_json() for step in self._selector_block[block_name][index]} except KeyError: _log.debug('Have not received data for Selector Block {} at Edit Selector {}'.format(block_name, index)) return None def _process_point_value(self, point_value): """ A PointValue was received from the Master. Process its payload. :param point_value: A PointValue. """ try: point_val = super(MesaAgent, self)._process_point_value(point_value) if point_val: if point_val.point_def.is_selector_block: self._current_block = { 'name': point_val.point_def.name, 'index': float(point_val.value) } _log.debug('Starting to receive Selector Block {name} at Edit Selector {index}'.format( **self._current_block )) # Publish mesa/point if the point action is PUBLISH or PUBLISH_AND_RESPOND if point_val.point_def.action in (PUBLISH, PUBLISH_AND_RESPOND): self.publish_point_value(point_value) self.update_function_for_point_value(point_val) if self._current_functions: for current_func_id, current_func in self._current_functions.items(): # if step action is ACTION_ECHO or ACTION_ECHO_AND_PUBLISH if current_func.has_input_point(): self.update_input_point( self.get_point_named(current_func.input_point_name()), point_val.unwrapped_value() ) # if step is the last curve or schedule step if self._current_block and point_val.point_def == current_func.definition.last_step.point_def: current_block_name = self._current_block['name'] self._selector_block.setdefault(current_block_name, dict()) self._selector_block[current_block_name][self._current_block['index']] = current_func.steps _log.debug('Saved Selector Block {} at Edit Selector {}: {}'.format( self._current_block['name'], self._current_block['index'], self.get_selector_block(self._current_block['name'], self._current_block['index']) )) self._current_block = dict() # if step reference to a curve or schedule function func_ref = current_func.last_step.definition.func_ref if func_ref: block_name = self.function_definitions[func_ref].first_step.name block_index = float(point_val.value) if not self._selector_block.get(block_name, dict()).get(block_index, None): error_msg = 'Have not received data for Selector Block {} at Edit Selector {}' raise DNP3Exception(error_msg.format(block_name, block_index)) current_edit_selector = { 'name': block_name, 'index': block_index } if current_edit_selector not in self._edit_selectors: self._edit_selectors.append(current_edit_selector) # if step action is ACTION_PUBLISH, ACTION_ECHO_AND_PUBLISH, or ACTION_PUBLISH_AND_RESPOND if current_func.publish_now(): self.publish_function_step(current_func.last_step) # if current function is completed if current_func.complete: self._current_functions.pop(current_func_id) self._edit_selectors = list() except (DNP3Exception, FunctionException) as err: self._current_functions = dict() self._edit_selectors = list() if type(err) == DNP3Exception: raise DNP3Exception('Error processing point value: {}'.format(err)) def update_function_for_point_value(self, point_value): """Add point_value to the current Function if appropriate.""" error_msg = None current_functions = self.current_function_for(point_value.point_def) if not current_functions: return None for function_id, current_function in current_functions.items(): try: if point_value.point_def.is_array_point: self.update_array_for_point(point_value) current_function.add_point_value(point_value, current_array=self._current_array, function_validation=self.function_validation) except (DNP3Exception, FunctionException) as err: current_functions.pop(function_id) if type(err) == DNP3Exception: error_msg = err if error_msg and not current_functions: raise DNP3Exception('Error updating function: {}'.format(error_msg)) def current_function_for(self, new_point_def): """A point was received. Return the current Function, updating it if necessary.""" new_point_function_def = self.function_definitions.get_fdef_for_pdef(new_point_def) if new_point_function_def is None: return None if self._current_functions: current_funcs = dict() for func_def in new_point_function_def: val = self._current_functions.pop(func_def.function_id, None) if val: current_funcs.update({func_def.function_id: val}) self._current_functions = current_funcs else: for func_def in new_point_function_def: if not self.all_functions_supported_by_default and not func_def.supported: raise DNP3Exception('Received a point for unsupported {}'.format(func_def)) self._current_functions[func_def.function_id] = Function(func_def) return self._current_functions 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). """ super(MesaAgent, self).update_input_point(point_def, value) if type(value) != list: # Side-effect: If it's a Support point for a Function, update the Function's "supported" property. func = self.function_definitions.support_point_names().get(point_def.name, None) if func is not None and func.supported != value: _log.debug('Updating supported property to {} in {}'.format(value, func)) func.supported = value def publish_function_step(self, step_to_send): """A Function Step was received from the DNP3 Master. Publish the Function.""" function_to_send = step_to_send.function points = {step.definition.name: step.as_json() for step in function_to_send.steps} for edit_selector in self._edit_selectors: block_name = edit_selector['name'] index = edit_selector['index'] try: points[block_name][index] = self.get_selector_block(block_name, index) except (KeyError, TypeError): points[block_name] = { index: self.get_selector_block(block_name, index) } msg = { "function_id": function_to_send.definition.function_id, "function_name": function_to_send.definition.name, "points": points } if step_to_send.definition.action == ACTION_PUBLISH_AND_RESPOND: msg["expected_response"] = step_to_send.definition.response _log.info('Publishing MESA {} message {}'.format(function_to_send, msg)) self.publish_data(self.function_topic, msg)
class MesaAgent(BaseDNP3Agent): """ MesaAgent is a VOLTTRON agent that handles MESA-ESS DNP3 outstation communications. MesaAgent models a DNP3 outstation, communicating with a DNP3 master. For further information about this agent, MESA-ESS, and DNP3 communications, please see the VOLTTRON MESA-ESS agent specification, which can be found in VOLTTRON readthedocs at http://volttron.readthedocs.io/en/develop/specifications/mesa_agent.html. This agent can be installed from a command-line shell as follows: $ export VOLTTRON_ROOT=<volttron github install directory> $ cd $VOLTTRON_ROOT $ source services/core/DNP3Agent/install_mesa_agent.sh That file specifies a default agent configuration, which can be overridden as needed. """ def __init__(self, functions=None, function_topic='', outstation_status_topic='', all_functions_supported_by_default=False, local_function_definitions_path=None, function_validation=False, **kwargs): """Initialize the MESA agent.""" super(MesaAgent, self).__init__(**kwargs) self.functions = functions self.function_topic = function_topic self.outstation_status_topic = outstation_status_topic self.all_functions_supported_by_default = all_functions_supported_by_default self.function_validation = function_validation # Update default config self.default_config.update({ 'functions': functions, 'function_topic': function_topic, 'outstation_status_topic': outstation_status_topic, 'all_functions_supported_by_default': all_functions_supported_by_default, 'function_validation': function_validation }) # Update default config in config store. self.vip.config.set_default('config', self.default_config) self.function_definitions = None self._local_function_definitions_path = local_function_definitions_path self._current_functions = dict() # {function_id: Function} self._current_block = dict() # {name: name, index: index} self._selector_block = dict( ) # {selector_block_point_name: {selector_index: [Step]}} self._edit_selectors = list() # [{name: name, index: index}] def _configure_parameters(self, contents): """ Initialize/Update the MesaAgent configuration. See also the superclass version of this method, which does most of the initialization. MesaAgent configuration parameters: functions: (string) A JSON structure of function definitions to be loaded. function_topic: (string) Message bus topic to use when publishing MESA-ESS functions. Default: mesa/function. all_functions_supported_by_default: (boolean) When deciding whether to reject points for unsupported functions, ignore the values of their 'supported' points: simply treat all functions as supported. Default: False. """ config = super(MesaAgent, self)._configure_parameters(contents) self.functions = config.get('functions', {}) self.function_topic = config.get('function_topic', DEFAULT_FUNCTION_TOPIC) self.all_functions_supported_by_default = config.get( 'all_functions_supported_by_default', False) self.function_validation = config.get('function_validation', False) _log.debug('MesaAgent configuration parameters:') _log.debug('\tfunctions type={}'.format(type(self.functions))) _log.debug('\tfunction_topic={}'.format(self.function_topic)) _log.debug('\tall_functions_supported_by_default={}'.format( bool(self.all_functions_supported_by_default))) _log.debug('\tfuntion_validation={}'.format( bool(self.function_validation))) self.load_function_definitions() self.supported_functions = [] # Un-comment the next line to do more detailed validation and print definition statistics. # validate_definitions(self.point_definitions, self.function_definitions) def load_function_definitions(self): """Populate the FunctionDefinitions repository from JSON in the config store.""" _log.debug('Loading MESA function definitions') try: self.function_definitions = FunctionDefinitions( self.point_definitions) self.function_definitions.load_functions( self.functions['functions']) except (AttributeError, TypeError) as err: if self._local_function_definitions_path: _log.warning( "Attempting to load Function Definitions from local path.") self.function_definitions = FunctionDefinitions( self.point_definitions, function_definitions_path=self. _local_function_definitions_path) else: raise DNP3Exception( "Failed to load Function Definitions from config store: {}" .format(err)) @RPC.export def reset(self): """Reset the agent's internal state, emptying point value caches. Used during iterative testing.""" super(MesaAgent, self).reset() self._current_functions = dict() self._current_block = dict() self._selector_block = dict() self._edit_selectors = list() @RPC.export def get_selector_block(self, block_name, index): try: return { step.definition.name: step.as_json() for step in self._selector_block[block_name][index] } except KeyError: _log.debug( 'Have not received data for Selector Block {} at Edit Selector {}' .format(block_name, index)) return None def _process_point_value(self, point_value): """ A PointValue was received from the Master. Process its payload. :param point_value: A PointValue. """ try: point_val = super(MesaAgent, self)._process_point_value(point_value) if point_val: if point_val.point_def.is_selector_block: self._current_block = { 'name': point_val.point_def.name, 'index': float(point_val.value) } _log.debug( 'Starting to receive Selector Block {name} at Edit Selector {index}' .format(**self._current_block)) # Publish mesa/point if the point action is PUBLISH or PUBLISH_AND_RESPOND if point_val.point_def.action in (PUBLISH, PUBLISH_AND_RESPOND): self.publish_point_value(point_value) self.update_function_for_point_value(point_val) if self._current_functions: for current_func_id, current_func in self._current_functions.items( ): # if step action is ACTION_ECHO or ACTION_ECHO_AND_PUBLISH if current_func.has_input_point(): self.update_input_point( self.get_point_named( current_func.input_point_name()), point_val.unwrapped_value()) # if step is the last curve or schedule step if self._current_block and point_val.point_def == current_func.definition.last_step.point_def: current_block_name = self._current_block['name'] self._selector_block.setdefault( current_block_name, dict()) self._selector_block[current_block_name][ self. _current_block['index']] = current_func.steps _log.debug( 'Saved Selector Block {} at Edit Selector {}: {}' .format( self._current_block['name'], self._current_block['index'], self.get_selector_block( self._current_block['name'], self._current_block['index']))) self._current_block = dict() # if step reference to a curve or schedule function func_ref = current_func.last_step.definition.func_ref if func_ref: block_name = self.function_definitions[ func_ref].first_step.name block_index = float(point_val.value) if not self._selector_block.get( block_name, dict()).get(block_index, None): error_msg = 'Have not received data for Selector Block {} at Edit Selector {}' raise DNP3Exception( error_msg.format(block_name, block_index)) current_edit_selector = { 'name': block_name, 'index': block_index } if current_edit_selector not in self._edit_selectors: self._edit_selectors.append( current_edit_selector) # if step action is ACTION_PUBLISH, ACTION_ECHO_AND_PUBLISH, or ACTION_PUBLISH_AND_RESPOND if current_func.publish_now(): self.publish_function_step(current_func.last_step) # if current function is completed if current_func.complete: self._current_functions.pop(current_func_id) self._edit_selectors = list() except (DNP3Exception, FunctionException) as err: self._current_functions = dict() self._edit_selectors = list() if type(err) == DNP3Exception: raise DNP3Exception( 'Error processing point value: {}'.format(err)) def update_function_for_point_value(self, point_value): """Add point_value to the current Function if appropriate.""" error_msg = None current_functions = self.current_function_for(point_value.point_def) if not current_functions: return None for function_id, current_function in current_functions.items(): try: if point_value.point_def.is_array_point: self.update_array_for_point(point_value) current_function.add_point_value( point_value, current_array=self._current_array, function_validation=self.function_validation) except (DNP3Exception, FunctionException) as err: current_functions.pop(function_id) if type(err) == DNP3Exception: error_msg = err if error_msg and not current_functions: raise DNP3Exception( 'Error updating function: {}'.format(error_msg)) def current_function_for(self, new_point_def): """A point was received. Return the current Function, updating it if necessary.""" new_point_function_def = self.function_definitions.get_fdef_for_pdef( new_point_def) if new_point_function_def is None: return None if self._current_functions: current_funcs = dict() for func_def in new_point_function_def: val = self._current_functions.pop(func_def.function_id, None) if val: current_funcs.update({func_def.function_id: val}) self._current_functions = current_funcs else: for func_def in new_point_function_def: if not self.all_functions_supported_by_default and not func_def.supported: raise DNP3Exception( 'Received a point for unsupported {}'.format(func_def)) self._current_functions[func_def.function_id] = Function( func_def) return self._current_functions 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). """ super(MesaAgent, self).update_input_point(point_def, value) if type(value) != list: # Side-effect: If it's a Support point for a Function, update the Function's "supported" property. func = self.function_definitions.support_point_names().get( point_def.name, None) if func is not None and func.supported != value: _log.debug('Updating supported property to {} in {}'.format( value, func)) func.supported = value def publish_function_step(self, step_to_send): """A Function Step was received from the DNP3 Master. Publish the Function.""" function_to_send = step_to_send.function points = { step.definition.name: step.as_json() for step in function_to_send.steps } for edit_selector in self._edit_selectors: block_name = edit_selector['name'] index = edit_selector['index'] try: points[block_name][index] = self.get_selector_block( block_name, index) except (KeyError, TypeError): points[block_name] = { index: self.get_selector_block(block_name, index) } msg = { "function_id": function_to_send.definition.function_id, "function_name": function_to_send.definition.name, "points": points } if step_to_send.definition.action == ACTION_PUBLISH_AND_RESPOND: msg["expected_response"] = step_to_send.definition.response _log.info('Publishing MESA {} message {}'.format( function_to_send, msg)) self.publish_data(self.function_topic, msg)