diff --git a/dnp3-master/service/config/device_ip_port_config.json b/dnp3-master/service/config/device_ip_port_config.json index 37c285a..04f56a1 100644 --- a/dnp3-master/service/config/device_ip_port_config.json +++ b/dnp3-master/service/config/device_ip_port_config.json @@ -1,12 +1,12 @@ { - "RTU1": { + "RTU1": { "name": "RTU1", "conversion_type": "RTU1", "CIM object": "l50", - "port": "20000", - "ip": "172.20.128.199", + "port": "30000", + "ip": "127.0.0.1", "desc": "RTU1", - "link_local_addr": "101", - "link_remote_addr": "43" + "link_local_addr": "2", + "link_remote_addr": "1" } -} +} \ No newline at end of file diff --git a/dnp3-master/service/config/device_ip_port_config.json.archive b/dnp3-master/service/config/device_ip_port_config.json.archive new file mode 100644 index 0000000..f3952ab --- /dev/null +++ b/dnp3-master/service/config/device_ip_port_config.json.archive @@ -0,0 +1,22 @@ +{ + "RTU1": { + "name": "RTU1", + "conversion_type": "RTU1", + "CIM object": "l50", + "port": "30000", + "ip": "127.0.0.1", + "desc": "RTU1", + "link_local_addr": "2", + "link_remote_addr": "1" + }, + "RTU2": { + "name": "RTU2", + "conversion_type": "RTU2", + "CIM object": "l50", + "port": "40000", + "ip": "127.0.0.1", + "desc": "RTU2", + "link_local_addr": "2", + "link_remote_addr": "1" + } +} \ No newline at end of file diff --git a/dnp3-master/service/config/mrid_object_outstation_index_dict_receive.json b/dnp3-master/service/config/mrid_object_outstation_index_dict_receive.json new file mode 100644 index 0000000..3fbc6c7 --- /dev/null +++ b/dnp3-master/service/config/mrid_object_outstation_index_dict_receive.json @@ -0,0 +1,4 @@ +{ + "5e6dfa84-705b-495a-8497-1c230541b636": 0, + "c3a4f467-fe35-4dce-b0b7-ab1f2f0a4c5d": 1 +} \ No newline at end of file diff --git a/dnp3-master/service/config/mrid_object_outstation_index_dict_send.json b/dnp3-master/service/config/mrid_object_outstation_index_dict_send.json new file mode 100644 index 0000000..a636ed3 --- /dev/null +++ b/dnp3-master/service/config/mrid_object_outstation_index_dict_send.json @@ -0,0 +1,4 @@ +{ + "1d88371e-3db5-4b23-b8cd-fc0f2f3569fb": 0, + "a9eb2b35-c340-4d31-9a0a-4ff2d4463361": 1 +} \ No newline at end of file diff --git a/dnp3-master/service/config/new_conversion_dict_master.json b/dnp3-master/service/config/new_conversion_dict_master.json new file mode 100644 index 0000000..27c905a --- /dev/null +++ b/dnp3-master/service/config/new_conversion_dict_master.json @@ -0,0 +1,62 @@ +{ + "RTU1": { + "Binary input": { + "0": { + "index": 0, + "original_name": "ufls_59.1", + "Type of Object": "Protection Relay", + "CIM attribute": "FunctionBlock.enabled", + "CIM name": "ufls_59.1", + "CIM phase": "ABC", + "CIM Variable": "Pos", + "CIM units": "", + "CIM type": "Pos", + "Multiplier": 1, + "CIM Var type": "magnitude" + }, + "1": { + "index": 1, + "original_name": "ufls_59.5", + "Type of Object": "Protection Relay", + "CIM attribute": "FunctionBlock.enabled", + "CIM name": "ufls_59.5", + "CIM phase": "ABC", + "CIM Variable": "Pos", + "CIM units": "", + "CIM type": "Pos", + "Multiplier": 1, + "CIM Var type": "magnitude" + } + }, + "Binary output": { + "0": { + "index": 0, + "original_name": "ufls_59.1", + "Type of Object": "Protection Relay", + "CIM attribute": "FunctionBlock.enabled", + "CIM name": "ufls_59.1", + "CIM phase": "ABC", + "CIM Variable": "pos", + "CIM units": "", + "CIM type": "Pos", + "Multiplier": 1, + "CIM Value Type": "magnitude", + "CIM mRID": "1d88371e-3db5-4b23-b8cd-fc0f2f3569fb" + }, + "1": { + "index": 1, + "original_name": "ufls_59.5", + "Type of Object": "Protection Relay", + "CIM attribute": "FunctionBlock.enabled", + "CIM name": "ufls_59.5", + "CIM phase": "ABC", + "CIM Variable": "pos", + "CIM units": "", + "CIM type": "Pos", + "Multiplier": 1, + "CIM Value Type": "magnitude", + "CIM mRID": "a9eb2b35-c340-4d31-9a0a-4ff2d4463361" + } + } +} +} diff --git a/dnp3-master/service/config/new_measurement_dict_master.json b/dnp3-master/service/config/new_measurement_dict_master.json new file mode 100644 index 0000000..edfa17b --- /dev/null +++ b/dnp3-master/service/config/new_measurement_dict_master.json @@ -0,0 +1,22 @@ +{ +"ufls_59.1": { + "Pos": { + "ABC": [ + { + "mrid": "1d88371e-3db5-4b23-b8cd-fc0f2f3569fb", + "type": "value" + } + ] + } + }, + "ufls_59.5": { + "Pos": { + "ABC": [ + { + "mrid": "a9eb2b35-c340-4d31-9a0a-4ff2d4463361", + "type": "value" + } + ] + } + } +} \ No newline at end of file diff --git a/dnp3-master/service/config/new_model_line_dict.json b/dnp3-master/service/config/new_model_line_dict.json new file mode 100644 index 0000000..3c036f8 --- /dev/null +++ b/dnp3-master/service/config/new_model_line_dict.json @@ -0,0 +1,23 @@ +{ +"ufls_59.1": { + "Pos": { + "ABC": [ + { + "mrid": "1d88371e-3db5-4b23-b8cd-fc0f2f3569fb", + "type": "value" + } + ] + } +}, +"ufls_59.5": { + "Pos": { + "ABC": [ + { + "mrid": "a9eb2b35-c340-4d31-9a0a-4ff2d4463361", + "type": "value" + } + ] +} +} +} + diff --git a/dnp3-master/service/gapp_send_cim_message.py b/dnp3-master/service/gapp_send_cim_message.py new file mode 100644 index 0000000..0ef241b --- /dev/null +++ b/dnp3-master/service/gapp_send_cim_message.py @@ -0,0 +1,65 @@ +import logging +import time + +from gridappsd import GridAPPSD +from gridappsd.topics import field_input_topic + +logging.basicConfig(level=logging.DEBUG) +_log = logging.getLogger(__name__) + + +gapps = GridAPPSD() + +assert gapps.connected + +# # Note we are sending the function not executing the function in the second parameter +topic = field_input_topic() +# gapps.subscribe(topic, on_message_callback) +# # gapps.subscribe('/topic/goss.gridappsd.field.input', on_message_callback) + +# gapps.send(topic, "A message about subscription lalalalalal") + +# gapps.send(topic, {"key": "value =======lal"}) # or use key-value pair as message + +# example_cim_message +cim_message = { + "command": "update", + "input": { + "message": { + "timestamp": 1736530734.662945, + "difference_mrid": "ccbc9b44-c6b6-4b89-b876-f9472867a2ca", + "reverse_differences": [ + { + "object": "5e6dfa84-705b-495a-8497-1c230541b636", + "attribute": "FunctionBlock.enabled", + "value": True + }, + { + "object": "c3a4f467-fe35-4dce-b0b7-ab1f2f0a4c5d", + "attribute": "FunctionBlock.enabled", + "value": False + } + ], + "forward_differences": [ + { + "object": "5e6dfa84-705b-495a-8497-1c230541b636", + "attribute": "FunctionBlock.enabled", + "value": False + }, + { + "object": "c3a4f467-fe35-4dce-b0b7-ab1f2f0a4c5d", + "attribute": "FunctionBlock.enabled", + "value": True + } + ] + }, + "simulation_id": "field_data" + } +} + + +# print(cim_message["input"]["message"]["timestamp"]) # 1357048800 +# print( +# cim_message["input"]["message"]["forward_differences"] +# ) # [{'object': '61A547FB-9F68-5635-BB4C-F7F537FD824E', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': 'E3CA4CD4-B0D4-9A83-3E2F-18AC5F1B55BA', 'attribute': 'ShuntCompensator.sections', 'value': 1}] +gapps.send(topic, cim_message) diff --git a/dnp3-master/service/start_service.py b/dnp3-master/service/start_service.py index fc142e0..935ec1a 100755 --- a/dnp3-master/service/start_service.py +++ b/dnp3-master/service/start_service.py @@ -39,6 +39,7 @@ import numpy as np import yaml import logging +import gridappsd.topics as topics from dnp3.CIMPro_AIAO_BIBO import CIMProcessor @@ -50,6 +51,27 @@ from gridappsd.topics import simulation_output_topic, simulation_input_topic from gridappsd import GridAPPSD, DifferenceBuilder, utils +# from dnp3_python.dnp3station.master import MyMaster +from dnp3_python.dnp3station.master_new import MyMasterNew + +# TODO: clean up the custom logger later +# Create a logger object +logger = logging.getLogger('my_logger') +logger.setLevel(logging.DEBUG) # Set the minimum logging level + +# Create a file handler for outputting logs to a file +file_handler = logging.FileHandler('/home/shared_user/gridappsd-dnp3-master/dnp3-master/service/myfile.log') +file_handler.setLevel(logging.INFO) # Only log error and above messages to the file + +# Create a formatter +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +file_handler.setFormatter(formatter) + +# Add handlers to the logger +logger.addHandler(file_handler) + +# TODO: clean up the custom logger later === (END) + myCIMProcessor = None logging.basicConfig(level=logging.DEBUG) @@ -72,8 +94,10 @@ def run_master(device_ip_port_config_all_13bus, names,simulation_id,gapps,dnp3_t convertion_type=device_ip_port_dict[ 'conversion_type'] object_name=device_ip_port_dict['CIM object'] + + logger.info(f"{HOST=}") # TODO: kefei added - application_1 = MyMaster(HOST=HOST, # "127.0.0.1 + application_1: MyMaster = MyMaster(HOST=HOST, # "127.0.0.1 LOCAL="0.0.0.0", PORT=int(PORT), DNP3_ADDR=int(DNP3_ADDR), @@ -82,11 +106,26 @@ def run_master(device_ip_port_config_all_13bus, names,simulation_id,gapps,dnp3_t listener=AppChannelListener(), soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim,gapps), master_application=MasterApplication()) - application_1.name=name + # TODO: kefei comment: (from dnp3.master_pnnl import MyMaster) MyMaster doesn't have def start method. How is it going to start? + master_application = MyMasterNew(port=40000) + master_application.start() + # master_ip=d_args.get("master_ip="), + # outstation_ip=d_args.get("outstation_ip="), + # port=d_args.get("port="), + # master_id=d_args.get("master_id="), + # outstation_id=d_args.get("outstation_id="), + + # # channel_log_level=opendnp3.levels.ALL_COMMS, + # # master_log_level=opendnp3.levels.ALL_COMMS + # # soe_handler=SOEHandler(soehandler_log_level=logging.DEBUG) + # ) + # master_application = MyMasterNew() + application_1.name=name # TODO: check if this is valid # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) _log.debug('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) - masters.append(application_1) + # masters.append(application_1) + masters.append(master_application) pv_points = [] for key in conversion_dict: @@ -108,7 +147,8 @@ def run_master(device_ip_port_config_all_13bus, names,simulation_id,gapps,dnp3_t myCIMProcessor = CIMProcessor(pv_points,application_1) - gapps.subscribe('/topic/goss.gridappsd.field.input', on_message) + # gapps.subscribe('/topic/goss.gridappsd.field.input', on_message) + gapps.subscribe(topics.field_input_topic(), on_message) SLEEP_SECONDS = 1 time.sleep(SLEEP_SECONDS) @@ -119,17 +159,98 @@ def run_master(device_ip_port_config_all_13bus, names,simulation_id,gapps,dnp3_t msg_count=0 csv_dict = {} - cim_full_msg = {'simulation_id': simulation_id, 'message':{'timestamp': int(time.time()),'measurements':{}}} + cim_full_msg = {'message':{'timestamp': int(time.time()),'measurements':{}}} + + + + + # while True: + # current_time = time.time() + # for master in masters: + # master.send_scan_all_request() + # # master_soe_handler: SOEHandler = master.soe_handler + # # cim_msg = master_soe_handler.get_msg() + # cim_msg = master.soe_handler.db # TODO: note this is place where cim_msg should be formatted + # # cim_msg = master.master_application.get_config() + # cim_full_msg['message']['measurements'].update(cim_msg) + # cim_full_msg['message']['timestamp'] = str(int(current_time)) + # _log.debug(f'Publishing CIM measurement: {json.dumps(cim_full_msg)}') + # # gapps.send('/topic/goss.gridappsd.field.output', json.dumps(cim_full_msg)) + # gapps.send(topics.field_output_topic(), json.dumps(cim_full_msg)) + # logger.info(f"{cim_full_msg=}") # TODO: kefei added + # msg_count+=1 + # time.sleep(2) + + # """ # example message: https://gridappsd.readthedocs.io/en/master/using_gridappsd/index.html?highlight=cim#subscribe-to-simulation-output + { + "simulation_id" : "12ae2345", + "message" : { + "timestamp" : "1357048800", + "measurements" : { + "123a456b-789c-012d-345e-678f901a234b":{ + "measurement_mrid" : "123a456b-789c-012d-345e-678f901a234b", + "value": 1 + # "magnitude" : 3410.456, + # "angle" : -123.456 + } + } + } + } + + + # in /home/shared_user/gridappsd-dnp3-master/dnp3-master/service/config/new_measurement_dict_master.json + new_measurement_dict_master = { + "ufls_59.1": { + "Pos": { + "ABC": [ + { + "mrid": "1d88371e-3db5-4b23-b8cd-fc0f2f3569fb", + "type": "value" + } + ] + } + }, + "ufls_59.5": { + "Pos": { + "ABC": [ + { + "mrid": "a9eb2b35-c340-4d31-9a0a-4ff2d4463361", + "type": "value" + } + ] + } + } + } + + def register_mapping(register_name: str, db_data): + """# mapping based on register name, e.g., ufls_59.1" -> "BinaryOutputStatus"[0]""" + if register_name == "ufls_59.1": + return db_data["BinaryOutputStatus"][0] + else: + return db_data["BinaryOutputStatus"][1] + while True: current_time = time.time() - for master in masters: - cim_msg = master.soe_handler.get_msg() - cim_full_msg['message']['measurements'].update(cim_msg) + for k, v in new_measurement_dict_master.items(): + master_application.send_scan_all_request() + # master_soe_handler: SOEHandler = master.soe_handler + # cim_msg = master_soe_handler.get_msg() + db_data = master_application.soe_handler.db + mr_id = v["Pos"]["ABC"][0]["mrid"] + value = register_mapping(k, db_data) + value=1 if value else 0 # convert to 1 or 0 (originally True or False) cim_full_msg['message']['timestamp'] = str(int(current_time)) - _log.debug(f'Publishing CIM measurement: {json.dumps(cim_full_msg)}') - gapps.send('/topic/goss.gridappsd.field.output', json.dumps(cim_full_msg)) - + cim_full_msg['message']['measurements'] = { + mr_id:{ + "measurement_mrid" : mr_id, + "value": value + }} + _log.debug(f'Publishing CIM measurement XXXX: {json.dumps(cim_full_msg)}') + # _log.debug(f"{master_application.soe_handler.db=}") + # gapps.send('/topic/goss.gridappsd.field.output', json.dumps(cim_full_msg)) + gapps.send(topics.field_output_topic(), json.dumps(cim_full_msg)) + # logger.info(f"{cim_full_msg=}") # TODO: kefei added msg_count+=1 time.sleep(2) @@ -162,6 +283,7 @@ def run_master(device_ip_port_config_all_13bus, names,simulation_id,gapps,dnp3_t #TODO: Change dummy simulation id to field id simulation_id='field_data' + # gapps = GridAPPSD(stomp_address="10.15.223.157", stomp_port=61613) gapps = GridAPPSD() gapps.connect() diff --git a/dnp3-master/service/start_service_new.py b/dnp3-master/service/start_service_new.py new file mode 100755 index 0000000..01faa0c --- /dev/null +++ b/dnp3-master/service/start_service_new.py @@ -0,0 +1,356 @@ +# Copyright (c) 2019 Alliance for Sustainable Energy, LLC +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#########################Edited/Prepared this code by Dr.Venkateswara Reddy Motakatla, NREL (VenkateswaraReddy.Motakatla@nrel.gov) +######################### PNNL Technical support: Poorva, and Alka +from __future__ import annotations + +import argparse +import csv +import json +import logging +import os +import platform +import sys +import time +from dataclasses import dataclass + +import gridappsd.topics as topics +import numpy as np +import yaml +from dnp3.CIMPro_AIAO_BIBO import CIMProcessor + +# from dnp3.master_pnnl import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.dnp3_to_cim import CIMMapping +from dnp3.points import PointValue + +# from dnp3_python.dnp3station.master import MyMaster +from dnp3_python.dnp3station.master_new import MyMasterNew +from gridappsd import DifferenceBuilder, GridAPPSD, utils +from gridappsd.topics import ( + field_input_topic,field_output_topic, + simulation_input_topic, + simulation_output_topic, +) +from pydnp3 import opendnp3, openpal + +# Setup argparse +parser = argparse.ArgumentParser() +parser.add_argument( + "outstation_names", nargs="+", help="name of dnp3 outstation", type=str +) +parser.add_argument( + "--config_path", + help="Path of the folder containing input configuration files", + default="config", + type=str, +) + +# Parse arguments globally +args = parser.parse_args() + +# Now args is available globally within the script +outstation_names = args.outstation_names +config_path = args.config_path + + +@dataclass +class RTUConfig: + """ + A configuration class for RTU (Remote Terminal Unit) devices. + + This class holds all the necessary configuration details for an RTU, + allowing easy access and management of its properties. It also includes + a static method to load configurations from a JSON file, simplifying + initialization and reducing potential setup errors. + + Attributes: + name (str): The name of the RTU. + conversion_type (str): Type of conversion used by the RTU. + CIM_object (str): CIM object identifier. + port (int): Network port number the RTU uses for communication. + ip (str): IP address of the RTU. + desc (str): A brief description of the RTU. + link_local_addr (int): Local network address used for linking. + link_remote_addr (int): Remote network address used for linking. + """ + + name: str + conversion_type: str + CIM_object: str + port: int + ip: str + desc: str + link_local_addr: int + link_remote_addr: int + + @staticmethod + def load_json_config(file_path: str, rtu_key: str) -> RTUConfig: + """ + Load RTU configuration from a JSON file. + + :param file_path: Path to the JSON file containing the RTU configurations. + :param rtu_key: Key of the RTU entry to be loaded from the JSON file. + :return: An instance of RTUConfig initialized with data from the JSON file. + + # Example usage: Assuming the JSON file is located at 'config.json' + # and the desired RTU key is 'RTU1' + rtu_config = RTUConfig.load_json_config('config.json', 'RTU1') + if rtu_config: + print(rtu_config) + + Example config file: + { + "RTU1": { + "name": "RTU1", + "conversion_type": "RTU1", + "CIM object": "l50", + "port": "30000", + "ip": "127.0.0.1", + "desc": "RTU1", + "link_local_addr": "2", + "link_remote_addr": "1" + }, + "RTU2": { + "name": "RTU2", + "conversion_type": "RTU2", + "CIM object": "l50", + "port": "40000", + "ip": "127.0.0.1", + "desc": "RTU2", + "link_local_addr": "2", + "link_remote_addr": "1" + } + } + """ + try: + with open(file_path, "r") as file: + data = json.load(file) + rtu_data = data[rtu_key] + return RTUConfig( + name=rtu_data["name"], + conversion_type=rtu_data["conversion_type"], + CIM_object=rtu_data["CIM object"], + port=int(rtu_data["port"]), + ip=rtu_data["ip"], + desc=rtu_data["desc"], + link_local_addr=int(rtu_data["link_local_addr"]), + link_remote_addr=int(rtu_data["link_remote_addr"]), + ) + + except (FileNotFoundError, KeyError, json.JSONDecodeError) as e: + raise Exception(f"Error loading RTU configuration: {e}") + + +myCIMProcessor = None + +logging.basicConfig(level=logging.DEBUG) +_log = logging.getLogger(__name__) + + +def on_message_control_outstation_binaryOutput(headers, message): + """ + A callback function when the receiving message from a subscribed topic. + message: a cim-difference message + """ + _log.debug(f"{headers = }") + _log.debug(f"{message = }") + timestamp = message["input"]["message"]["timestamp"] # 1357048800 + forward_differences = message[ + "input" + ][ + "message" + ][ + "forward_differences" + ] # [{'object': '61A547FB-9F68-5635-BB4C-F7F537FD824E', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': 'E3CA4CD4-B0D4-9A83-3E2F-18AC5F1B55BA', 'attribute': 'ShuntCompensator.sections', 'value': 1}] + _log.debug(f"{timestamp = }") + _log.debug(f"{forward_differences = }") + + # register_to_db_index: dict[str, int] = { + # "61A547FB-9F68-5635-BB4C-F7F537FD824E": 0, + # "E3CA4CD4-B0D4-9A83-3E2F-18AC5F1B55BA": 1, + # } # TODO: confirm what should be the correct mrid(s). + with open(os.path.join(config_path, "mrid_object_outstation_index_dict_receive.json")) as f: + register_to_db_index_receive = json.load(f) + rtu = outstation_names[ + 0 + ] # TODO: assume only one RTU device for now (not sure if will demo multiple devcies) + for command in forward_differences: + master_app: MyMasterNew = master_apps[rtu] + mrid = command["object"] # aka object in the cim-difference-message + index = register_to_db_index_receive[mrid] + val_to_set = ( + True if command["value"] == True else False + ) # TODO: Make sure if True (Enable) -> True, False (Disable) -> False. It is possible gridapps-d has different interpretation of True/Enable vs. dnp3 + master_app.send_direct_point_command( + group=10, + variation=2, + index=index, + val_to_set=val_to_set, + ) # Note: group10Variation2 is for BinaryOutput, hardcoded here for demo purposes + # result = master_application.get_db_by_group_variation(group=10, variation=2) + # print("SUCCESS", {"BinaryOutputStatus": list(result.values())[0]}) + + +# def _register_mapping(register_name: str, db_data): +# """# mapping based on register name, e.g., ufls_59.1" -> "BinaryOutputStatus"[0]""" +# if register_name == "ufls_59.1": +# return db_data["BinaryOutputStatus"][0] +# else: +# return db_data["BinaryOutputStatus"][1] + + +def _construct_cim_full_msg( + db_data, + cim_full_msg=None, +) -> dict: + """ + Constructs a CIM message with measurement data. + Args: + cim_full_msg (dict, optional): Initial structure of the CIM message. Defaults to None. + Returns: + dict: A dictionary containing the CIM message structured with measurements. + Example message: + { + "simulation_id" : "12ae2345", + "message" : { + "timestamp" : "1357048800", + "measurements" : { + "123a456b-789c-012d-345e-678f901a234b":{ + "measurement_mrid" : "123a456b-789c-012d-345e-678f901a234b", + "value": True # enable is True, disable is False + # "magnitude" : 3410.456, + # "angle" : -123.456 + } + } + } + } + """ + if cim_full_msg is None: + cim_full_msg = {"message": {}} + + # Load measurement definitions from a JSON file. + # config_path = "path_to_config" # Define or import the configuration path. + with open(os.path.join(config_path, "new_measurement_dict_master.json")) as f: + new_measurement_dict_master = json.load(f) + + with open(os.path.join(config_path, "mrid_object_outstation_index_dict_send.json")) as f: + register_to_db_index_send = json.load(f) + + # Populate the CIM message with the latest measurements. + for key, value in new_measurement_dict_master.items(): + mr_id = value["Pos"]["ABC"][0]["mrid"] + # Assuming _register_mapping and db_data are defined elsewhere + value = ( + True if db_data["BinaryOutputStatus"][register_to_db_index_send[mrid]] else False + ) # Note: db_data can be None + current_time = int(time.time()) + cim_full_msg["message"]["timestamp"] = str(current_time) + cim_full_msg["message"]["measurements"] = { + mr_id: {"measurement_mrid": mr_id, "value": value} + } + # Log the prepared message + _log.debug(f"Publishing CIM measurement XXXX: {json.dumps(cim_full_msg)}") + + return cim_full_msg + + +def run_master(): + # init master station(s) + config_path = args.config_path + _log.debug(f"{config_path = }") + # Note: in current design master_apps is a collection of master applications. + # Make it global accessible to use in gridapps-d on_message callback workflow + global master_apps + master_apps = {} + for outstaion_name in outstation_names: + config: RTUConfig = RTUConfig.load_json_config( + os.path.join(config_path, "device_ip_port_config.json"), outstaion_name + ) + master_app = MyMasterNew( + master_ip="0.0.0.0", + outstation_ip=config.ip, + port=config.port, + master_id=config.link_local_addr, + outstation_id=config.link_remote_addr, + ) + master_app.start() + master_apps[outstaion_name] = master_app + + # subscribe to gridapps-d topic to receive control command + topic = field_input_topic() # topic = '/topic/goss.gridappsd.field.input' + gapps.subscribe( + topic, on_message_control_outstation_binaryOutput + ) + # loop to poll data from outstation, construct a cim-message and send it + msg_count = 0 + while True: + for outstation_name, master_app in master_apps.items(): + # Send scan request and process data + # time.sleep(100) # TODO: clean this. + master_app.send_scan_all_request() + db_data = master_app.soe_handler.db + _log.debug(f"{db_data =}") + + try: + cim_full_msg = _construct_cim_full_msg(db_data) + except Exception as e: + _log.error(f"Failed to construct CIM message: {e}") + continue # Continue with the next iteration if an error occurs. + + # Sending the cim-message + # _log.info(f"Sending CIM message: {cim_full_msg}") + gapps.send( + topics.field_output_topic(), json.dumps(cim_full_msg) + ) + msg_count += 1 + time.sleep( + 5 + ) # Wait for 2 seconds before the next cycle (to poll data from outstation) + + +if __name__ == "__main__": + # simulation_id = "field_data" # TODO: Confirm what simulation_id is for. Change dummy simulation id to field id (if it is needed) + # gapps = GridAPPSD(stomp_address="10.15.223.157", stomp_port=61613) + gapps = GridAPPSD() + gapps.connect() + + # with open(config_path + "/device_ip_port_config.json") as f: + # device_ip_port_config_all_Xcel = json.load(f) + + # dnp3_to_cim = CIMMapping( + # conversion_dict=os.path.join(config_path, "conversion_dict_master_data.json"), + # model_line_dict=os.path.join(config_path, "measurement_dict_master.json"), + # ) + + # conversion_dict = dnp3_to_cim.conversion_dict + # # print(conversion_dict) + + time.sleep(1) + + run_master() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e364785 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,58 @@ +argcomplete==3.5.0 +asttokens==2.4.1 +backcall==0.2.0 +certifi==2024.8.30 +charset-normalizer==3.3.2 +cim-graph @ git+https://github.com/PNNL-CIM-Tools/CIM-Graph.git@edb08512573ca1084e92808231a86be21cad98be +comm==0.2.2 +dateutils==0.6.12 +debugpy==1.8.5 +decorator==5.1.1 +defusedxml==0.7.1 +dnp3-python==0.2.3b3 +docopt==0.6.2 +exceptiongroup==1.2.2 +executing==2.1.0 +gridappsd-python==2024.8.1a1 +idna==3.8 +ipykernel==6.29.5 +ipython==8.12.3 +isodate==0.6.1 +jedi==0.19.1 +jupyter_client==8.6.2 +jupyter_core==5.7.2 +matplotlib-inline==0.1.7 +mysql-connector-python==8.4.0 +neo4j==5.24.0 +nest-asyncio==1.6.0 +numpy==2.1.1 +oxrdflib==0.3.7 +packaging==24.1 +pandas==2.2.2 +parso==0.8.4 +pexpect==4.9.0 +pickleshare==0.7.5 +platformdirs==4.3.2 +prompt_toolkit==3.0.47 +psutil==6.0.0 +ptyprocess==0.7.0 +pure_eval==0.2.3 +Pygments==2.18.0 +pyoxigraph==0.3.22 +pyparsing==3.1.4 +python-dateutil==2.9.0.post0 +pytz==2022.7.1 +PyYAML==6.0.2 +pyzmq==26.2.0 +rdflib==7.0.0 +rdflib-neo4j==0.0.1b6 +requests==2.28.2 +six==1.16.0 +SPARQLWrapper==2.0.0 +stack-data==0.6.3 +stomp.py==6.0.0 +tornado==6.4.1 +traitlets==5.14.3 +tzdata==2024.1 +urllib3==1.26.20 +wcwidth==0.2.13