From bfd84b9528f6c4fe7a020eb24fc62e8187d3bd70 Mon Sep 17 00:00:00 2001 From: poorva1209 Date: Wed, 13 May 2020 11:42:16 -0700 Subject: [PATCH 01/64] testing --- dnp3/service/dnp3/port.json | 8 ++-- dnp3/service/new_start_service.py | 74 +++++++++++++++++-------------- 2 files changed, 45 insertions(+), 37 deletions(-) mode change 100644 => 100755 dnp3/service/dnp3/port.json mode change 100644 => 100755 dnp3/service/new_start_service.py diff --git a/dnp3/service/dnp3/port.json b/dnp3/service/dnp3/port.json old mode 100644 new mode 100755 index 8bd5af6..cd98a4f --- a/dnp3/service/dnp3/port.json +++ b/dnp3/service/dnp3/port.json @@ -1,12 +1,12 @@ [ { "port":"20000", - "link_local_addr":"1", - "link_remote_addr":"1024" + "link_local_addr":1, + "link_remote_addr":1024 }, { "port":"20001", - "link_local_addr":"2", - "link_remote_addr":"1025" + "link_local_addr":2, + "link_remote_addr":1025 } ] diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py old mode 100644 new mode 100755 index 81babbf..58437e3 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -275,17 +275,14 @@ def start_outstation(outstation_config, processor): print("*********************************") print(str(outstation_config)) #dnp3_outstation = DNP3Outstation('0.0.0.0', 20000, outstation_config) - outstation_list =[] - for m in port_config: - print(m['port']) - dnp3_outstation = DNP3Outstation('0.0.0.0', int(m['port']), outstation_config) - dnp3_outstation.set_agent(processor) - dnp3_outstation.start() - outstation_list.append(dnp3_outstation) + dnp3_outstation = DNP3Outstation('0.0.0.0', outstation_config['port'], outstation_config) + dnp3_outstation.set_agent(processor) + dnp3_outstation.start() + outstation_list.append(dnp3_outstation) _log.debug('DNP3 initialization complete. In command loop.') # Ad-hoc tests can be performed at this point if desired. - return outstation_list + return outstation def load_point_definitions(self): @@ -325,38 +322,49 @@ def publish_outstation_status(status_string): parser.add_argument('simulation_id', help="Simulation id") opts = parser.parse_args() simulation_id = opts.simulation_id + + with open("/tmp/port.json", 'r') as f: + port_config = json.load(f) + print(port_config) filepath = "/tmp/gridappsd_tmp/{}/model_dict.json".format(simulation_id) with open(filepath, 'r') as fp: cim_dict = json.load(fp) - dnp3_object = DNP3Mapping(cim_dict) - dnp3_object._create_dnp3_object_map() - - with open("/tmp/json_out", 'w') as fp: - out_dict = dict({'points': dnp3_object.out_json}) - json.dump(out_dict, fp, indent=2, sort_keys=True) - - with open("/tmp/port.json", 'r') as f: - port_config = json.load(f) - print(port_config) - - if not dnp3_object.out_json: - sys.stderr.write("invalid points specified in json configuration file.") - sys.exit(10) - + + gapps = GridAPPSD(opts.simulation_id, address=utils.get_gridappsd_address(), username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) - gapps.subscribe(simulation_output_topic(opts.simulation_id),dnp3_object.on_message) - + - oustation = dict() - point_def = PointDefinitions() - point_def.load_points(dnp3_object.out_json) - processor = Processor(point_def, simulation_id, gapps) - dnp3_object.load_point_def(point_def) - outstation_list = start_outstation(oustation, processor) - for outstation in outstation_list: - dnp3_object.load_outstation(outstation) + + dnp3_object_list = [] + check_valid_points = True + + for obj in port_config: + dnp3_object = DNP3Mapping(cim_dict) + dnp3_object._create_dnp3_object_map() + gapps.subscribe(simulation_output_topic(opts.simulation_id),dnp3_object.on_message) + + if check_valid_points: + + with open("/tmp/json_out", 'w') as fp: + out_dict = dict({'points': dnp3_object.out_json}) + json.dump(out_dict, fp, indent=2, sort_keys=True) + + if not dnp3_object.out_json: + sys.stderr.write("invalid points specified in json configuration file.") + sys.exit(10) + + check_valid_points = False + + oustation = obj + point_def = PointDefinitions() + point_def.load_points(dnp3_object.out_json) + processor = Processor(point_def, simulation_id, gapps) + dnp3_object.load_point_def(point_def) + outstation = start_outstation(oustation, processor) + for outstation in outstation_list: + dnp3_object.load_outstation(outstation) # gapps.send(simulation_input_topic(opts.simulation_id), processor.process_point_value()) try: From 5bace6048073eb68e5a06fe69ff8a2cabd1076d9 Mon Sep 17 00:00:00 2001 From: singha42 Date: Thu, 14 May 2020 18:01:19 +0000 Subject: [PATCH 02/64] updated new_start code --- dnp3/service/new_start_service.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index 58437e3..3bd4a1c 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -278,11 +278,10 @@ def start_outstation(outstation_config, processor): dnp3_outstation = DNP3Outstation('0.0.0.0', outstation_config['port'], outstation_config) dnp3_outstation.set_agent(processor) dnp3_outstation.start() - outstation_list.append(dnp3_outstation) _log.debug('DNP3 initialization complete. In command loop.') # Ad-hoc tests can be performed at this point if desired. - return outstation + return dnp3_outstation def load_point_definitions(self): @@ -363,8 +362,8 @@ def publish_outstation_status(status_string): processor = Processor(point_def, simulation_id, gapps) dnp3_object.load_point_def(point_def) outstation = start_outstation(oustation, processor) - for outstation in outstation_list: - dnp3_object.load_outstation(outstation) + #for outstation in outstation_list: + dnp3_object.load_outstation(outstation) # gapps.send(simulation_input_topic(opts.simulation_id), processor.process_point_value()) try: From ffd99a9e0b7f8e58473a23fc4ff33aa5e9d48e74 Mon Sep 17 00:00:00 2001 From: singha42 Date: Mon, 18 May 2020 22:32:32 +0000 Subject: [PATCH 03/64] updated codes --- dnp3/service/dnp3/cim_to_dnp3.py | 61 +++++++++++++++--------------- dnp3/service/dnp3/outstation.py | 64 ++++++++++++++++++-------------- 2 files changed, 66 insertions(+), 59 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 739da32..a23667f 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -35,7 +35,7 @@ class DNP3Mapping(): - """ This creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" + """ This class creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" def __init__(self, map_file): self.c_ao = 0 @@ -46,7 +46,7 @@ def __init__(self, map_file): self.out_json = list() self.file_dict = map_file self.processor_point_def = PointDefinitions() - self.outstation = DNP3Outstation('',0,'') + self.outstation = DNP3Outstation('','','') def on_message(self, simulation_id,message): @@ -62,7 +62,7 @@ def on_message(self, simulation_id,message): try: message_str = 'received message ' + str(message) - + print('Outstation {}'.format(str(self.outstation))) json_msg = yaml.safe_load(str(message)) if type(json_msg) != dict: @@ -75,20 +75,25 @@ def on_message(self, simulation_id,message): # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values for y in measurement_values: + #print(y.keys()) # print(self.processor_point_def.points_by_mrid()) m = measurement_values[y] if "magnitude" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): - #print("point",point) - #print("y",y) + #print("Outcheck2",self.outstation) if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): point.magnitude = m.get("magnitude") + print('Measurementloop', self.outstation) self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + #print("Outcheck3",point.index) + print("apply update working", self.outstation) elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): - point.value = m.get("value") - self.outstation.apply_update(opendnp3.Binary(point.value), point.index) + point.value = m.get("value") + print("binary check") + self.outstation.apply_update(opendnp3.Binary(point.value), point.index) + print("binary update working") except Exception as e: message_str = "An error occurred while trying to translate the message received" + str(e) @@ -159,7 +164,6 @@ def _create_dnp3_object_map(self): fuses = list() breakers = list() reclosers = list() - energyconsumers = list() for x in feeders: measurements = x.get("measurements", []) capacitors = x.get("capacitors", []) @@ -170,14 +174,14 @@ def _create_dnp3_object_map(self): fuses = x.get("fuses", []) breakers = x.get("breakers", []) reclosers = x.get("reclosers", []) - energyconsumers = x.get("energyconsumers", []) for m in measurements: attribute = attribute_map['regulators']['attribute'] measurement_type = m.get("measurementType") measurement_id = m.get("mRID") name= measurement_id.replace('-', '').replace('_', '') - description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") + if m['MeasurementClass'] == "Analog": self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) self.c_ai += 1 @@ -190,8 +194,8 @@ def _create_dnp3_object_map(self): else: self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) self.c_di += 1 - - + + for m in capacitors: measurement_id = m.get("mRID") cap_attribute = attribute_map['capacitors']['attribute'] # type: List[str] @@ -211,20 +215,25 @@ def _create_dnp3_object_map(self): for m in regulators: reg_attribute = attribute_map['regulators']['attribute'] - # bank_phase = list(m['bankPhases']) - for n in range(0, 4): - measurement_id = m.get("mRID") - name = uuid.uuid4().hex - description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] - self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) - self.c_ao += 1 + bank_phase = list(m['bankPhases']) + #print(f"m {m}") for i in range(5, 7): for j in range(0, len(m['bankPhases'])): - measurement_id = m.get("mRID")[j] + measurement_id = m.get("mRID") name = uuid.uuid4().hex - description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] + description = "Name:" + m['bankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",controlAttribute:" + reg_attribute[i] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) self.c_ao += 1 + for n in range(0, 4): + measurement_id = m.get("mRID") + name = uuid.uuid4().hex + #print("Alka",bank_phase[n]) + reg_attribute = attribute_map['regulators']['attribute'] + #print(reg_attribute) + description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" + ",Attribute:" + reg_attribute[n] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) + self.c_ao += 1 + for m in solarpanels: measurement_id = m.get("mRID") @@ -280,15 +289,5 @@ def _create_dnp3_object_map(self): self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 - - for m in energyconsumers: - measurement_id = m.get("mRID") - for k in range(0, len(m['phases'])): - phase_value = list(m['phases']) - name = uuid.uuid4().hex - description = "EnergyConsumer, " + m["name"] + "Phase: - " + phase_value[k] - self.assign_val_a("AI", 30, 1, self.c_ai, name, description, None , measurement_id) - self.c_ai += 1 - return self.out_json diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index 5c25e57..d52b07d 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -112,12 +112,13 @@ def __init__(self, local_ip, port, outstation_config): def start(self): _log.debug('Configuring the DNP3 stack.') + _log.debug(str(self.outstation_config)) #for m in self.port_config: self.stack_config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(self.outstation_config.get('database_sizes', 10000))) _log.debug(self.stack_config) self.stack_config.outstation.eventBufferConfig = opendnp3.EventBufferConfig.AllTypes(self.outstation_config.get('event_buffers', 10)) - self.stack_config.outstation.params.allowUnsolicited = self.outstation_config.get('allow_unsolicited', False) + self.stack_config.outstation.params.allowUnsolicited = self.outstation_config.get('allow_unsolicited', True) #for x in self.port_config: # link_addr = m['link_local_addr'] # remote_addr = m['link_remote_addr'] @@ -131,7 +132,6 @@ def start(self): _log.debug(db_config) for point in self.get_agent().point_definitions.all_points(): #_log.debug("Adding Point: {}".format(point)) - # print(point) if point.point_type == 'Analog Input': cfg = db_config.analog[int(point.index)] elif point.point_type == 'Binary Input': @@ -150,28 +150,29 @@ def start(self): # self.log_handler = MyLogger().Create() self.log_handler = MyLogger() self.manager = asiodnp3.DNP3Manager(threads_to_allocate, self.log_handler) + #print('self', self.manager) _log.debug('Creating the DNP3 channel, a TCP server.') self.retry_parameters = asiopal.ChannelRetry().Default() # self.listener = asiodnp3.PrintingChannelListener().Create() # (or use this during regression testing) self.listener = AppChannelListener() - print(self.listener) - print("Starting TCP server 2000000") + #print(self.listener) + #print("Starting TCP server 2000000") self.channel = self.manager.AddTCPServer("server", self.dnp3_log_level(), self.retry_parameters, self.local_ip, self.port, self.listener) - + + + #_log.debug(str(self.channel)) _log.debug('Adding the DNP3 Outstation to the channel.') # self.command_handler = opendnp3.SuccessCommandHandler().Create() # (or use this during regression testing) self.command_handler = OutstationCommandHandler() self.outstation = self.channel.AddOutstation("outstation", self.command_handler, self, self.stack_config) - # Set the singleton instance that communicates with the Master. self.set_outstation(self.outstation) - _log.info('Enabling the DNP3 Outstation. Traffic can now start to flow.') self.outstation.Enable() @@ -194,31 +195,33 @@ def set_agent(cls, agent): """Set the singleton DNP3Agent """ cls.agent = agent - @classmethod - def get_outstation(cls): + #@classmethod + def get_outstation(self): """Get the singleton instance of IOutstation.""" - outst = cls.outstation + outst = self.outstation + #print('Alkaaa',outst) if outst is None: raise AttributeError('IOutstation is not yet enabled') return outst - @classmethod - def set_outstation(cls, outstn): + #@classmethod + def set_outstation(self, outstn): """ Set the singleton instance of IOutstation, as returned from the channel's AddOutstation call. Making IOutstation available as a singleton allows other classes to send commands to it -- see apply_update(). """ - cls.outstation = outstn + self.outstation = outstn + print("oustn", outstn) - @classmethod - def get_outstation_config(cls): + #classmethod + def get_outstation_config(self): """Get the outstation_config, a dictionary of configuration parameters.""" - return cls.outstation_config + return self.outstation_config - @classmethod - def set_outstation_config(cls, outstn_cfg): + #@classmethod + def set_outstation_config(self, outstn_cfg): """ Set the outstation_config. @@ -226,7 +229,7 @@ def set_outstation_config(cls, outstn_cfg): :param outstn_cfg: A dictionary of configuration parameters. """ - cls.outstation_config = outstn_cfg + self.outstation_config = outstn_cfg def dnp3_log_level(self): """ @@ -299,8 +302,8 @@ def WarmRestartSupport(self): _log.debug('In DNP3 WarmRestartSupport') return opendnp3.RestartMode.UNSUPPORTED - @classmethod - def apply_update(cls, value, index): + #@classmethod + def apply_update(self, value, index): """ Record an opendnp3 data value (Analog, Binary, etc.) in the outstation's database. @@ -310,15 +313,20 @@ def apply_update(cls, value, index): :param index: (integer) Index of the data definition in the opendnp3 database. """ #_log.debug('Recording DNP3 {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value)) - max_index = cls.get_outstation_config().get('database_sizes', 10000) - if index > max_index: - raise ValueError('Attempt to set a value for index {} which exceeds database size {}'.format(index, - max_index)) + + #max_index = cls.get_outstation_config().get('database_sizes', 10000) + print("outstation is :", self.get_outstation_config()) + print("check 4", index) + #if index > max_index: + # raise ValueError('Attempt to set a value for index {} which exceeds database size {}'.format(index,max_index)) + builder = asiodnp3.UpdateBuilder() builder.Update(value, index) update = builder.Build() try: - cls.get_outstation().Apply(update) + print('alkaaaaa', self.get_outstation()) + self.get_outstation().Apply(update) + print("Updating point values", self.get_outstation()) except AttributeError as err: if not os.environ.get('UNITTEST', False): raise err @@ -413,8 +421,8 @@ def Log(self, entry): message = entry.message _log.debug('DNP3Log {0}\t(filters={1}) {2}'.format(location, filters, message)) # This is here as an example of how to send a specific log entry to the message bus as outstation status. - # if 'Accepted connection' in message or 'Listening on' in message: - # DNP3Outstation.get_agent().publish_outstation_status(str(message)) + if 'Accepted connection' in message or 'Listening on' in message: + DNP3Outstation.get_agent().publish_outstation_status(str(message)) def main(): From 0119a31006786c54d183c14307fda3ad8dbb3c4f Mon Sep 17 00:00:00 2001 From: singha42 Date: Tue, 19 May 2020 22:14:33 +0000 Subject: [PATCH 04/64] Updated codes --- dnp3/service/dnp3/cim_to_dnp3.py | 16 ++++++---------- dnp3/service/dnp3/outstation.py | 21 ++++++++++++--------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index a23667f..c258190 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -62,7 +62,6 @@ def on_message(self, simulation_id,message): try: message_str = 'received message ' + str(message) - print('Outstation {}'.format(str(self.outstation))) json_msg = yaml.safe_load(str(message)) if type(json_msg) != dict: @@ -75,25 +74,22 @@ def on_message(self, simulation_id,message): # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values for y in measurement_values: - #print(y.keys()) - # print(self.processor_point_def.points_by_mrid()) m = measurement_values[y] if "magnitude" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): - #print("Outcheck2",self.outstation) + print("Outcheck2",self.outstation.get_agent()) if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): point.magnitude = m.get("magnitude") print('Measurementloop', self.outstation) self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - #print("Outcheck3",point.index) print("apply update working", self.outstation) elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): - if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): - point.value = m.get("value") - print("binary check") - self.outstation.apply_update(opendnp3.Binary(point.value), point.index) - print("binary update working") + #if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): + point.value = m.get("value") + print("binary check") + self.outstation.apply_update(opendnp3.Binary(point.value), point.index) + print("binary update working") except Exception as e: message_str = "An error occurred while trying to translate the message received" + str(e) diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index d52b07d..b332281 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -118,7 +118,7 @@ def start(self): self.stack_config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(self.outstation_config.get('database_sizes', 10000))) _log.debug(self.stack_config) self.stack_config.outstation.eventBufferConfig = opendnp3.EventBufferConfig.AllTypes(self.outstation_config.get('event_buffers', 10)) - self.stack_config.outstation.params.allowUnsolicited = self.outstation_config.get('allow_unsolicited', True) + self.stack_config.outstation.params.allowUnsolicited = self.outstation_config.get('allow_unsolicited', False) #for x in self.port_config: # link_addr = m['link_local_addr'] # remote_addr = m['link_remote_addr'] @@ -131,6 +131,7 @@ def start(self): db_config = self.stack_config.dbConfig _log.debug(db_config) for point in self.get_agent().point_definitions.all_points(): + print("Agent is", self.get_agent()) #_log.debug("Adding Point: {}".format(point)) if point.point_type == 'Analog Input': cfg = db_config.analog[int(point.index)] @@ -182,18 +183,19 @@ def reload_parameters(self, local_ip, port, outstation_config): self.port = port self.outstation_config = outstation_config - @classmethod - def get_agent(cls): + #@classmethod + def get_agent(self): """Return the singleton DNP3Agent """ - agt = cls.agent + agt = self.agent + print('agt is', agt) if agt is None: raise ValueError('Outstation has no configured agent') return agt - @classmethod - def set_agent(cls, agent): + #@classmethod + def set_agent(self, agent): """Set the singleton DNP3Agent """ - cls.agent = agent + self.agent = agent #@classmethod def get_outstation(self): @@ -324,9 +326,9 @@ def apply_update(self, value, index): builder.Update(value, index) update = builder.Build() try: - print('alkaaaaa', self.get_outstation()) + print('printing at apply_update', self.get_outstation()) self.get_outstation().Apply(update) - print("Updating point values", self.get_outstation()) + #print("Updating point values", self.get_outstation()) except AttributeError as err: if not os.environ.get('UNITTEST', False): raise err @@ -403,6 +405,7 @@ def OnStateChange(self, state): :param state: A ChannelState. """ DNP3Outstation.get_agent().publish_outstation_status(str(state)) + #self.get_agent().publish_outstation_status(str(state)) # class MyLogger(asiodnp3.ConsoleLogger): From efcb3bbb497b409ff76671d3e6c32feb76dea72d Mon Sep 17 00:00:00 2001 From: poorva1209 Date: Thu, 21 May 2020 12:49:49 -0700 Subject: [PATCH 05/64] updated to use self.get_agent() --- dnp3/service/dnp3/outstation.py | 37 +++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index b332281..3fb6edb 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -131,7 +131,7 @@ def start(self): db_config = self.stack_config.dbConfig _log.debug(db_config) for point in self.get_agent().point_definitions.all_points(): - print("Agent is", self.get_agent()) + #print("Agent is", self.get_agent()) #_log.debug("Adding Point: {}".format(point)) if point.point_type == 'Analog Input': cfg = db_config.analog[int(point.index)] @@ -149,28 +149,34 @@ def start(self): threads_to_allocate = self.outstation_config.get('threads_to_allocate', 1) # self.log_handler = asiodnp3.ConsoleLogger().Create() # (or use this during regression testing) # self.log_handler = MyLogger().Create() - self.log_handler = MyLogger() + self.log_handler = MyLogger(self) self.manager = asiodnp3.DNP3Manager(threads_to_allocate, self.log_handler) #print('self', self.manager) _log.debug('Creating the DNP3 channel, a TCP server.') self.retry_parameters = asiopal.ChannelRetry().Default() # self.listener = asiodnp3.PrintingChannelListener().Create() # (or use this during regression testing) - self.listener = AppChannelListener() + self.listener = AppChannelListener(self) #print(self.listener) #print("Starting TCP server 2000000") + print('**********************************************') + print(self.retry_parameters) + print(self.local_ip) + print(self.port) + print(self.listener) + print('**********************************************') self.channel = self.manager.AddTCPServer("server", self.dnp3_log_level(), self.retry_parameters, self.local_ip, - self.port, + int(self.port), self.listener) #_log.debug(str(self.channel)) _log.debug('Adding the DNP3 Outstation to the channel.') # self.command_handler = opendnp3.SuccessCommandHandler().Create() # (or use this during regression testing) - self.command_handler = OutstationCommandHandler() + self.command_handler = OutstationCommandHandler(self) self.outstation = self.channel.AddOutstation("outstation", self.command_handler, self, self.stack_config) # Set the singleton instance that communicates with the Master. self.set_outstation(self.outstation) @@ -187,7 +193,7 @@ def reload_parameters(self, local_ip, port, outstation_config): def get_agent(self): """Return the singleton DNP3Agent """ agt = self.agent - print('agt is', agt) + #print('agt is', agt) if agt is None: raise ValueError('Outstation has no configured agent') return agt @@ -355,6 +361,9 @@ class OutstationCommandHandler(opendnp3.ICommandHandler): ICommandHandler implements the Outstation's handling of Select and Operate, which relay commands and data from the Master to the Outstation. """ + def __init__(self,dnp3Object): + super(OutstationCommandHandler, self).__init__() + self.dnp3Object = dnp3Object def Start(self): # This debug line is too chatty... @@ -375,11 +384,10 @@ def Select(self, command, index): :param index: int :return: CommandStatus """ - return DNP3Outstation.get_agent().process_point_value('Select', command, index, None) + return self.dnp3Object.get_agent().process_point_value('Select', command, index, None) def Operate(self, command, index, op_type): """ - The Master sent an Operate command to the Outstation. Handle it. :param command: ControlRelayOutputBlock, AnalogOutputInt16, AnalogOutputInt32, AnalogOutputFloat32, or AnalogOutputDouble64. @@ -387,7 +395,7 @@ def Operate(self, command, index, op_type): :param op_type: OperateType :return: CommandStatus """ - return DNP3Outstation.get_agent().process_point_value('Operate', command, index, op_type) + return self.dnp3Object.get_agent().process_point_value('Operate', command, index, op_type) class AppChannelListener(asiodnp3.IChannelListener): @@ -395,8 +403,10 @@ class AppChannelListener(asiodnp3.IChannelListener): IChannelListener has been overridden to implement application-specific channel behavior. """ - def __init__(self): + def __init__(self,dnp3Object): + super(AppChannelListener, self).__init__() + self.dnp3Object = dnp3Object def OnStateChange(self, state): """ @@ -404,7 +414,7 @@ def OnStateChange(self, state): :param state: A ChannelState. """ - DNP3Outstation.get_agent().publish_outstation_status(str(state)) + self.dnp3Object.get_agent().publish_outstation_status(str(state)) #self.get_agent().publish_outstation_status(str(state)) @@ -414,8 +424,9 @@ class MyLogger(openpal.ILogHandler): ILogHandler has been overridden to implement application-specific logging behavior. """ - def __init__(self): + def __init__(self, dnp3Object): super(MyLogger, self).__init__() + self.dnp3Object = dnp3Object def Log(self, entry): """Write a DNP3 log entry to the logger (debug level).""" @@ -425,7 +436,7 @@ def Log(self, entry): _log.debug('DNP3Log {0}\t(filters={1}) {2}'.format(location, filters, message)) # This is here as an example of how to send a specific log entry to the message bus as outstation status. if 'Accepted connection' in message or 'Listening on' in message: - DNP3Outstation.get_agent().publish_outstation_status(str(message)) + self.dnp3Object.get_agent().publish_outstation_status(str(message)) def main(): From 8c485bf5b1810edb5cbd325592124760810f3ce8 Mon Sep 17 00:00:00 2001 From: singha42 Date: Thu, 21 May 2020 23:16:18 +0000 Subject: [PATCH 06/64] updated codes --- dnp3/service/dnp3/cim_to_dnp3.py | 12 ++++-------- dnp3/service/dnp3/outstation.py | 4 ---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index c258190..b04b814 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -77,19 +77,15 @@ def on_message(self, simulation_id,message): m = measurement_values[y] if "magnitude" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): - print("Outcheck2",self.outstation.get_agent()) if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): point.magnitude = m.get("magnitude") - print('Measurementloop', self.outstation) self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - print("apply update working", self.outstation) + elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): - #if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): - point.value = m.get("value") - print("binary check") - self.outstation.apply_update(opendnp3.Binary(point.value), point.index) - print("binary update working") + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): + point.value = m.get("value") + self.outstation.apply_update(opendnp3.Binary(point.value), point.index) except Exception as e: message_str = "An error occurred while trying to translate the message received" + str(e) diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index 3fb6edb..ff1f517 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -221,7 +221,6 @@ def set_outstation(self, outstn): to send commands to it -- see apply_update(). """ self.outstation = outstn - print("oustn", outstn) #classmethod def get_outstation_config(self): @@ -323,8 +322,6 @@ def apply_update(self, value, index): #_log.debug('Recording DNP3 {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value)) #max_index = cls.get_outstation_config().get('database_sizes', 10000) - print("outstation is :", self.get_outstation_config()) - print("check 4", index) #if index > max_index: # raise ValueError('Attempt to set a value for index {} which exceeds database size {}'.format(index,max_index)) @@ -332,7 +329,6 @@ def apply_update(self, value, index): builder.Update(value, index) update = builder.Build() try: - print('printing at apply_update', self.get_outstation()) self.get_outstation().Apply(update) #print("Updating point values", self.get_outstation()) except AttributeError as err: From a63f371e380fe108c39b1895ac3944d9c9b04871 Mon Sep 17 00:00:00 2001 From: singha42 Date: Tue, 2 Jun 2020 23:42:32 +0000 Subject: [PATCH 07/64] added processor to new_start_service.py --- dnp3/service/dnp3/cim_to_dnp3.py | 48 ++++++++++++++++++++++---------- dnp3/service/dnp3/outstation.py | 7 ++--- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index b04b814..4821e9b 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -77,15 +77,19 @@ def on_message(self, simulation_id,message): m = measurement_values[y] if "magnitude" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): + #print("Outcheck2",point) if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): point.magnitude = m.get("magnitude") + #print("Outcheck2",point) self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): point.value = m.get("value") self.outstation.apply_update(opendnp3.Binary(point.value), point.index) + #print("Binary point", point) + #print("On message value", m.get("value")) + #print("Binary value", point.value) except Exception as e: message_str = "An error occurred while trying to translate the message received" + str(e) @@ -166,6 +170,7 @@ def _create_dnp3_object_map(self): fuses = x.get("fuses", []) breakers = x.get("breakers", []) reclosers = x.get("reclosers", []) + energyconsumers = x.get("energyconsumers", []) for m in measurements: attribute = attribute_map['regulators']['attribute'] @@ -194,34 +199,35 @@ def _create_dnp3_object_map(self): for l in range(0, 4): # publishing attribute value for capacitors as Bianry/Analog Input points based on phase attribute - name = uuid.uuid4().hex + #name = uuid.uuid4().hex + name = m['name'] description = "Name:" + m['name'] + "ConductingEquipment_type:LinearShuntCompensator" + ",Attribute:" + cap_attribute[l] + ",Phase:" + m['phases'] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, cap_attribute[l]) self.c_ao += 1 for p in range(0, len(m['phases'])): - name =uuid.uuid4().hex + #name =uuid.uuid4().hex + name = m['name'] description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[p] + ",Phase:" + m['phases'][p] - # description = "Capacitor, " + m['name'] + "," + "phase -" + m['phases'][p] + ", and attribute is - " + cap_attribute[4] self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) self.c_do += 1 for m in regulators: reg_attribute = attribute_map['regulators']['attribute'] bank_phase = list(m['bankPhases']) - #print(f"m {m}") for i in range(5, 7): for j in range(0, len(m['bankPhases'])): measurement_id = m.get("mRID") - name = uuid.uuid4().hex + #name = uuid.uuid4().hex + name = m['bankName'][j] description = "Name:" + m['bankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",controlAttribute:" + reg_attribute[i] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) self.c_ao += 1 + for n in range(0, 4): measurement_id = m.get("mRID") - name = uuid.uuid4().hex - #print("Alka",bank_phase[n]) + #name = uuid.uuid4().hex + name = m['bankName'] reg_attribute = attribute_map['regulators']['attribute'] - #print(reg_attribute) description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" + ",Attribute:" + reg_attribute[n] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) self.c_ao += 1 @@ -229,14 +235,16 @@ def _create_dnp3_object_map(self): for m in solarpanels: measurement_id = m.get("mRID") - name = uuid.uuid4().hex + #name = uuid.uuid4().hex + name = m['name'] description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) self.c_ao += 1 for m in batteries: measurement_id = m.get("mRID") - name = uuid.uuid4().hex + #name = uuid.uuid4().hex + name = m['name'] description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) self.c_ao += 1 @@ -246,7 +254,7 @@ def _create_dnp3_object_map(self): switch_attribute = attribute_map['switches']['attribute'] for k in range(0, len(m['phases'])): phase_value = list(m['phases']) - name = uuid.uuid4().hex + name = m['name'] description = "Name:" + m["name"] + ",ConductingEquipment_type:LoadBreakSwitch" + "Phase:" + phase_value[k] +",controlAttribute:"+switch_attribute self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 @@ -256,7 +264,7 @@ def _create_dnp3_object_map(self): switch_attribute = attribute_map['switches']['attribute'] for l in range(0, len(m['phases'])): phase_value = list(m['phases']) - name = uuid.uuid4().hex + name = m['name'] description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 @@ -266,7 +274,7 @@ def _create_dnp3_object_map(self): switch_attribute = attribute_map['switches']['attribute'] for n in range(0, len(m['phases'])): phase_value = list(m['phases']) - name =uuid.uuid4().hex + name = m['name'] description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 @@ -276,10 +284,20 @@ def _create_dnp3_object_map(self): switch_attribute = attribute_map['switches']['attribute'] for k in range(0, len(m['phases'])): phase_value = list(m['phases']) - name = uuid.uuid4().hex + name = m['name'] description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 + + for m in energyconsumers: + measurement_id = m.get("mRID") + #switch_attribute = attribute_map['switches']['attribute'] + for p in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "_Phase-" + phase_value[p] + description = "Energyconsumer-" + m["name"] + " Phase: - " + phase_value[p] + ",ConductingEquipment_type:EnergyConsumer" + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 return self.out_json diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index ff1f517..1ba3bed 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -193,7 +193,6 @@ def reload_parameters(self, local_ip, port, outstation_config): def get_agent(self): """Return the singleton DNP3Agent """ agt = self.agent - #print('agt is', agt) if agt is None: raise ValueError('Outstation has no configured agent') return agt @@ -207,7 +206,6 @@ def set_agent(self, agent): def get_outstation(self): """Get the singleton instance of IOutstation.""" outst = self.outstation - #print('Alkaaa',outst) if outst is None: raise AttributeError('IOutstation is not yet enabled') return outst @@ -221,6 +219,7 @@ def set_outstation(self, outstn): to send commands to it -- see apply_update(). """ self.outstation = outstn + print("oustn", outstn) #classmethod def get_outstation_config(self): @@ -319,7 +318,7 @@ def apply_update(self, value, index): :param value: An instance of Analog, Binary, or another opendnp3 data value. :param index: (integer) Index of the data definition in the opendnp3 database. """ - #_log.debug('Recording DNP3 {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value)) + #_log.debug('Recording DNP3 {} measurement, name={}, index={}, value={}'.format(type(value).__name__, value, index, value.value)) #max_index = cls.get_outstation_config().get('database_sizes', 10000) #if index > max_index: @@ -330,7 +329,7 @@ def apply_update(self, value, index): update = builder.Build() try: self.get_outstation().Apply(update) - #print("Updating point values", self.get_outstation()) + print("Updating point values", self.port) except AttributeError as err: if not os.environ.get('UNITTEST', False): raise err From 7809333ab956dd3d3bec7520a74e0625af389e8d Mon Sep 17 00:00:00 2001 From: singha42 Date: Tue, 2 Jun 2020 23:42:51 +0000 Subject: [PATCH 08/64] added processor to new_start_service.py --- dnp3/service/new_start_service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index 3bd4a1c..0106385 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -74,14 +74,14 @@ def process_point_value(self, command_type, command, index, op_type): if point.name in str(point_value.point_def) and point.attribute == 'Switch.open': if 'ON' in str(command.functionCode): self._diff.clear() - self._diff.add_difference(point.measurement_id, point.attribute, 1, 0) + self._diff.add_difference(point.measurement_id, point.attribute, 0, 1) msg = self._diff.get_message() self._gapps.send(self._publish_to_topic, json.dumps(msg)) print(json.dumps(msg)) else: self._diff.clear() - self._diff.add_difference(point.measurement_id, point.attribute, 0, 1) + self._diff.add_difference(point.measurement_id, point.attribute, 1,0) msg = self._diff.get_message() self._gapps.send(self._publish_to_topic, json.dumps(msg)) print(json.dumps(msg)) @@ -278,6 +278,7 @@ def start_outstation(outstation_config, processor): dnp3_outstation = DNP3Outstation('0.0.0.0', outstation_config['port'], outstation_config) dnp3_outstation.set_agent(processor) dnp3_outstation.start() + processor.outstation = dnp3_outstation _log.debug('DNP3 initialization complete. In command loop.') # Ad-hoc tests can be performed at this point if desired. From f2f0f483b4b4077a1401a99b061d3bc833429175 Mon Sep 17 00:00:00 2001 From: singha42 Date: Thu, 11 Jun 2020 01:08:00 +0000 Subject: [PATCH 09/64] updated for VAR value --- dnp3/service/dnp3/cim_to_dnp3.py | 188 ++++++++------- dnp3/service/dnp3/cim_to_dnp3.py-10June | 303 ++++++++++++++++++++++++ 2 files changed, 404 insertions(+), 87 deletions(-) create mode 100755 dnp3/service/dnp3/cim_to_dnp3.py-10June diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 4821e9b..2da5082 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -4,6 +4,7 @@ import datetime import random import uuid +import math from pydnp3 import opendnp3 from typing import List, Dict, Union, Any @@ -35,18 +36,19 @@ class DNP3Mapping(): - """ This class creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" + """ This creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" def __init__(self, map_file): self.c_ao = 0 self.c_do = 0 self.c_ai = 0 self.c_di = 0 + self.c_var = 0 self.measurements = dict() self.out_json = list() self.file_dict = map_file self.processor_point_def = PointDefinitions() - self.outstation = DNP3Outstation('','','') + self.outstation = DNP3Outstation('',0,'') def on_message(self, simulation_id,message): @@ -62,6 +64,7 @@ def on_message(self, simulation_id,message): try: message_str = 'received message ' + str(message) + json_msg = yaml.safe_load(str(message)) if type(json_msg) != dict: @@ -74,22 +77,31 @@ def on_message(self, simulation_id,message): # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values for y in measurement_values: + # print(self.processor_point_def.points_by_mrid()) m = measurement_values[y] if "magnitude" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): - #print("Outcheck2",point) + #print("point",point) + #print("y",y) if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): point.magnitude = m.get("magnitude") - #print("Outcheck2",point) self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "VAR" in point.name: + angle = math.radians(m.get("angle")) + point.magnitude = math.sin(angle) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type != "VA" and "VAR" not in point.name: + angle1 = math.radians(m.get("angle")) + point.magnitude = math.cos(angle1) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): - point.value = m.get("value") - self.outstation.apply_update(opendnp3.Binary(point.value), point.index) - #print("Binary point", point) - #print("On message value", m.get("value")) - #print("Binary value", point.value) + point.value = m.get("value") + self.outstation.apply_update(opendnp3.Binary(point.value), point.index) except Exception as e: message_str = "An error occurred while trying to translate the message received" + str(e) @@ -160,6 +172,7 @@ def _create_dnp3_object_map(self): fuses = list() breakers = list() reclosers = list() + energyconsumers = list() for x in feeders: measurements = x.get("measurements", []) capacitors = x.get("capacitors", []) @@ -176,9 +189,8 @@ def _create_dnp3_object_map(self): attribute = attribute_map['regulators']['attribute'] measurement_type = m.get("measurementType") measurement_id = m.get("mRID") - name= measurement_id.replace('-', '').replace('_', '') - description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") - + name= m['name'] + description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") if m['MeasurementClass'] == "Analog": self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) self.c_ai += 1 @@ -191,63 +203,65 @@ def _create_dnp3_object_map(self): else: self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) self.c_di += 1 - - + + for m in measurements: + attribute = attribute_map['regulators']['attribute'] + measurement_type = m.get("measurementType") + if m.get("measurementType") == "VA": + measurement_id = m.get("mRID") + name = m['name'] + '_' + 'VAR-value' + description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + if m['MeasurementClass'] == "Analog": + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) + self.c_ai += 1 + for m in capacitors: measurement_id = m.get("mRID") cap_attribute = attribute_map['capacitors']['attribute'] # type: List[str] for l in range(0, 4): # publishing attribute value for capacitors as Bianry/Analog Input points based on phase attribute - #name = uuid.uuid4().hex name = m['name'] description = "Name:" + m['name'] + "ConductingEquipment_type:LinearShuntCompensator" + ",Attribute:" + cap_attribute[l] + ",Phase:" + m['phases'] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, cap_attribute[l]) self.c_ao += 1 for p in range(0, len(m['phases'])): - #name =uuid.uuid4().hex - name = m['name'] + name =uuid.uuid4().hex description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[p] + ",Phase:" + m['phases'][p] + # description = "Capacitor, " + m['name'] + "," + "phase -" + m['phases'][p] + ", and attribute is - " + cap_attribute[4] self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) self.c_do += 1 - for m in regulators: - reg_attribute = attribute_map['regulators']['attribute'] - bank_phase = list(m['bankPhases']) - for i in range(5, 7): - for j in range(0, len(m['bankPhases'])): - measurement_id = m.get("mRID") - #name = uuid.uuid4().hex - name = m['bankName'][j] - description = "Name:" + m['bankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",controlAttribute:" + reg_attribute[i] - self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) - self.c_ao += 1 - - for n in range(0, 4): - measurement_id = m.get("mRID") - #name = uuid.uuid4().hex - name = m['bankName'] - reg_attribute = attribute_map['regulators']['attribute'] - description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" + ",Attribute:" + reg_attribute[n] - self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) - self.c_ao += 1 - - - for m in solarpanels: - measurement_id = m.get("mRID") - #name = uuid.uuid4().hex - name = m['name'] - description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id - self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) - self.c_ao += 1 - - for m in batteries: - measurement_id = m.get("mRID") - #name = uuid.uuid4().hex - name = m['name'] - description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" - self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) - self.c_ao += 1 + # for m in regulators: + # reg_attribute = attribute_map['regulators']['attribute'] + # # bank_phase = list(m['bankPhases']) + # for n in range(0, 4): + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] + # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) + # self.c_ao += 1 + # for i in range(5, 7): + # for j in range(0, len(m['bankPhases'])): + # measurement_id = m.get("mRID")[j] + # name = uuid.uuid4().hex + # description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] + # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) + # self.c_ao += 1 + # + # for m in solarpanels: + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id + # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + # self.c_ao += 1 + + # for m in batteries: + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" + # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + # self.c_ao += 1 for m in switches: measurement_id = m.get("mRID") @@ -259,45 +273,45 @@ def _create_dnp3_object_map(self): self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 - for m in fuses: - measurement_id = m.get("mRID") - switch_attribute = attribute_map['switches']['attribute'] - for l in range(0, len(m['phases'])): - phase_value = list(m['phases']) - name = m['name'] - description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id - self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - self.c_do += 1 + # for m in fuses: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for l in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name = uuid.uuid4().hex + # description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + + # for m in breakers: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for n in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name =uuid.uuid4().hex + # description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + # + # for m in reclosers: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for k in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name = uuid.uuid4().hex + # description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 - for m in breakers: - measurement_id = m.get("mRID") - switch_attribute = attribute_map['switches']['attribute'] - for n in range(0, len(m['phases'])): - phase_value = list(m['phases']) - name = m['name'] - description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute - self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - self.c_do += 1 - for m in reclosers: + for m in energyconsumers: measurement_id = m.get("mRID") - switch_attribute = attribute_map['switches']['attribute'] for k in range(0, len(m['phases'])): phase_value = list(m['phases']) name = m['name'] - description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute - self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - self.c_do += 1 - - for m in energyconsumers: - measurement_id = m.get("mRID") - #switch_attribute = attribute_map['switches']['attribute'] - for p in range(0, len(m['phases'])): - phase_value = list(m['phases']) - name = m['name'] + "_Phase-" + phase_value[p] - description = "Energyconsumer-" + m["name"] + " Phase: - " + phase_value[p] + ",ConductingEquipment_type:EnergyConsumer" - self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - self.c_do += 1 + description = "EnergyConsumer, " + m["name"] + "Phase: - " + phase_value[k] + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, None , measurement_id) + self.c_ai += 1 return self.out_json diff --git a/dnp3/service/dnp3/cim_to_dnp3.py-10June b/dnp3/service/dnp3/cim_to_dnp3.py-10June new file mode 100755 index 0000000..4821e9b --- /dev/null +++ b/dnp3/service/dnp3/cim_to_dnp3.py-10June @@ -0,0 +1,303 @@ +import json +import yaml +import sys +import datetime +import random +import uuid + +from pydnp3 import opendnp3 +from typing import List, Dict, Union, Any +from dnp3.outstation import DNP3Outstation +from dnp3.points import ( + PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT +) + +out_json = list() + +'''Dictionary for mapping the attribute values of control poitns for Capacitor, Regulator and Switches''' + +attribute_map = { + "capacitors": { + "attribute": ["RegulatingControl.mode", "RegulatingControl.targetDeadband", "RegulatingControl.targetValue", + "ShuntCompensator.aVRDelay", "ShuntCompensator.sections"]} + , + "switches": { + "attribute": "Switch.open" + } + , + + "regulators": { + "attribute": ["RegulatingControl.targetDeadband", "RegulatingControl.targetValue", "TapChanger.initialDelay", + "TapChanger.lineDropCompensation", "TapChanger.step", "TapChanger.lineDropR", + "TapChanger.lineDropX"]} + +} + + +class DNP3Mapping(): + """ This class creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" + + 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('','','') + + + def on_message(self, simulation_id,message): + """ This method handles incoming messages on the fncs_output_topic for the simulation_id. + Parameters + ---------- + headers: dict + A dictionary of headers that could be used to determine topic of origin and + other attributes. + message: object + + """ + + try: + message_str = 'received message ' + str(message) + json_msg = yaml.safe_load(str(message)) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + measurement_values = json_msg["message"]["measurements"] + + # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values + for y in measurement_values: + m = measurement_values[y] + if "magnitude" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + #print("Outcheck2",point) + if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): + point.magnitude = m.get("magnitude") + #print("Outcheck2",point) + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + elif "value" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): + point.value = m.get("value") + self.outstation.apply_update(opendnp3.Binary(point.value), point.index) + #print("Binary point", point) + #print("On message value", m.get("value")) + #print("Binary value", point.value) + except Exception as e: + message_str = "An error occurred while trying to translate the message received" + str(e) + + def assign_val_a(self, data_type, group, variation, index, name, description, measurement_type, measurement_id): + """ Method is to initialize parameters to be used for generating output points for measurement key values """ + records = dict() # type: Dict[str, Any] + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + records["measurement_type"] = measurement_type + records["measurement_id"] = measurement_id + records["magnitude"] = "0" + self.out_json.append(records) + + def assign_val_d(self, data_type, group, variation, index, name, description, measurement_id, attribute): + """ This method is to initialize parameters to be used for generating output points for output points""" + records = dict() + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + # records["measurement_type"] = measurement_type + records["measurement_id"] = measurement_id + records["attribute"] = attribute + records["value"] = "0" + self.out_json.append(records) + + def assign_valc(self, data_type, group, variation, index, name, description, measurement_id, attribute): + """ Method is to initialize parameters to be used for generating dnp3 control as Analog/Binary Input points""" + records = dict() + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + # records["measurement_type"] = measurement_type + records["attribute"] = attribute + records["measurement_id"] = measurement_id + self.out_json.append(records) + + def load_json(self, out_json, out_file): + with open(out_file, 'w') as fp: + out_dict = dict({'points': out_json}) + json.dump(out_dict, fp, indent=2, sort_keys=True) + + def load_point_def(self, point_def): + self.processor_point_def = point_def + + def load_outstation(self, outstation): + self.outstation = outstation + + def _create_dnp3_object_map(self): + """This method creates the points by taking the input data from model dictionary file""" + + feeders = self.file_dict.get("feeders", []) + measurements = list() + capacitors = list() + regulators = list() + switches = list() + solarpanels = list() + batteries = list() + fuses = list() + breakers = list() + reclosers = list() + for x in feeders: + measurements = x.get("measurements", []) + capacitors = x.get("capacitors", []) + regulators = x.get("regulators", []) + switches = x.get("switches", []) + solarpanels = x.get("solarpanels", []) + batteries = x.get("batteries", []) + fuses = x.get("fuses", []) + breakers = x.get("breakers", []) + reclosers = x.get("reclosers", []) + energyconsumers = x.get("energyconsumers", []) + + for m in measurements: + attribute = attribute_map['regulators']['attribute'] + measurement_type = m.get("measurementType") + measurement_id = m.get("mRID") + name= measurement_id.replace('-', '').replace('_', '') + description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") + + if m['MeasurementClass'] == "Analog": + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) + self.c_ai += 1 + + elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": + if "Reg" in m['name']: + for r in range(5, 7): + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) + self.c_ao += 1 + else: + self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) + self.c_di += 1 + + + for m in capacitors: + measurement_id = m.get("mRID") + cap_attribute = attribute_map['capacitors']['attribute'] # type: List[str] + + for l in range(0, 4): + # publishing attribute value for capacitors as Bianry/Analog Input points based on phase attribute + #name = uuid.uuid4().hex + name = m['name'] + description = "Name:" + m['name'] + "ConductingEquipment_type:LinearShuntCompensator" + ",Attribute:" + cap_attribute[l] + ",Phase:" + m['phases'] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, cap_attribute[l]) + self.c_ao += 1 + for p in range(0, len(m['phases'])): + #name =uuid.uuid4().hex + name = m['name'] + description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[p] + ",Phase:" + m['phases'][p] + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) + self.c_do += 1 + + for m in regulators: + reg_attribute = attribute_map['regulators']['attribute'] + bank_phase = list(m['bankPhases']) + for i in range(5, 7): + for j in range(0, len(m['bankPhases'])): + measurement_id = m.get("mRID") + #name = uuid.uuid4().hex + name = m['bankName'][j] + description = "Name:" + m['bankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",controlAttribute:" + reg_attribute[i] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) + self.c_ao += 1 + + for n in range(0, 4): + measurement_id = m.get("mRID") + #name = uuid.uuid4().hex + name = m['bankName'] + reg_attribute = attribute_map['regulators']['attribute'] + description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" + ",Attribute:" + reg_attribute[n] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) + self.c_ao += 1 + + + for m in solarpanels: + measurement_id = m.get("mRID") + #name = uuid.uuid4().hex + name = m['name'] + description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id + self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + self.c_ao += 1 + + for m in batteries: + measurement_id = m.get("mRID") + #name = uuid.uuid4().hex + name = m['name'] + description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" + self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + self.c_ao += 1 + + for m in switches: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for k in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "Name:" + m["name"] + ",ConductingEquipment_type:LoadBreakSwitch" + "Phase:" + phase_value[k] +",controlAttribute:"+switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + for m in fuses: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for l in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + for m in breakers: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for n in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + for m in reclosers: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for k in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + for m in energyconsumers: + measurement_id = m.get("mRID") + #switch_attribute = attribute_map['switches']['attribute'] + for p in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "_Phase-" + phase_value[p] + description = "Energyconsumer-" + m["name"] + " Phase: - " + phase_value[p] + ",ConductingEquipment_type:EnergyConsumer" + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + return self.out_json + From a2226f22e4418163de9136f8cb165486c891a7d4 Mon Sep 17 00:00:00 2001 From: singha42 Date: Thu, 11 Jun 2020 01:47:59 +0000 Subject: [PATCH 10/64] updated for VAR value --- dnp3/service/dnp3/cim_to_dnp3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 2da5082..46e0e00 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -189,7 +189,7 @@ def _create_dnp3_object_map(self): attribute = attribute_map['regulators']['attribute'] measurement_type = m.get("measurementType") measurement_id = m.get("mRID") - name= m['name'] + name= m['name'] + '-' + m['phases'] description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") if m['MeasurementClass'] == "Analog": self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) @@ -209,7 +209,7 @@ def _create_dnp3_object_map(self): measurement_type = m.get("measurementType") if m.get("measurementType") == "VA": measurement_id = m.get("mRID") - name = m['name'] + '_' + 'VAR-value' + name = m['name'] + '-' + m['phases'] + '-VAR-value' description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") if m['MeasurementClass'] == "Analog": self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) From ffa5c527768cd329adfa83999e7004b897b68468 Mon Sep 17 00:00:00 2001 From: singha42 Date: Thu, 11 Jun 2020 21:31:31 +0000 Subject: [PATCH 11/64] updated for VAR value --- dnp3/service/dnp3/cim_to_dnp3.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 46e0e00..bf02a17 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -92,7 +92,7 @@ def on_message(self, simulation_id,message): point.magnitude = math.sin(angle) * m.get("magnitude") self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - elif point.measurement_type != "VA" and "VAR" not in point.name: + elif point.measurement_type != "VA" and "Watts" in point.name: angle1 = math.radians(m.get("angle")) point.magnitude = math.cos(angle1) * m.get("magnitude") self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) @@ -195,6 +195,24 @@ def _create_dnp3_object_map(self): self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) self.c_ai += 1 + if m.get("measurementType") == "VA": + measurement_id = m.get("mRID") + name1 = m['name'] + '-' + m['phases'] + '-VAR-value' + name2 = m['name'] + '-' + m['phases'] + '-Watts-value' + name3 = m['name'] + '-' + m['phases'] + '-angle' + + description1 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "VAR" + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + description2 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "Watt" + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + description3 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "angle" + ",ConnectivityNode:" + m.get("ConnectivityNode") + ",SimObject:" + m.get("SimObject") + if m['MeasurementClass'] == "Analog": + self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) + self.c_ai += 1 + + elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": if "Reg" in m['name']: for r in range(5, 7): @@ -204,16 +222,6 @@ def _create_dnp3_object_map(self): self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) self.c_di += 1 - for m in measurements: - attribute = attribute_map['regulators']['attribute'] - measurement_type = m.get("measurementType") - if m.get("measurementType") == "VA": - measurement_id = m.get("mRID") - name = m['name'] + '-' + m['phases'] + '-VAR-value' - description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") - if m['MeasurementClass'] == "Analog": - self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) - self.c_ai += 1 for m in capacitors: measurement_id = m.get("mRID") @@ -226,7 +234,7 @@ def _create_dnp3_object_map(self): self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, cap_attribute[l]) self.c_ao += 1 for p in range(0, len(m['phases'])): - name =uuid.uuid4().hex + name = m['name'] + m['phases'][p] description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[p] + ",Phase:" + m['phases'][p] # description = "Capacitor, " + m['name'] + "," + "phase -" + m['phases'][p] + ", and attribute is - " + cap_attribute[4] self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) From 60e8c5583d2e2a9484087d2ae0cad136e2be32ef Mon Sep 17 00:00:00 2001 From: singha42 Date: Thu, 11 Jun 2020 21:38:35 +0000 Subject: [PATCH 12/64] updated for VAR value --- dnp3/service/dnp3/cim_to_dnp3.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index bf02a17..3fbe147 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -92,10 +92,15 @@ def on_message(self, simulation_id,message): point.magnitude = math.sin(angle) * m.get("magnitude") self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - elif point.measurement_type != "VA" and "Watts" in point.name: + elif point.measurement_type == "VA" and "Watts" in point.name: angle1 = math.radians(m.get("angle")) point.magnitude = math.cos(angle1) * m.get("magnitude") self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "angle" in point.name: + angle2 = math.radians(m.get("angle")) + #point.magnitude = math.cos(angle1) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(angle2), point.index) elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): From 5279a222e0a8de4a461c5d2b8dbf96dad054f96a Mon Sep 17 00:00:00 2001 From: singha42 Date: Wed, 8 Jul 2020 13:32:08 -0400 Subject: [PATCH 13/64] cim_to_dnp3.py --- dnp3/service/dnp3/cim_to_dnp3.py | 222 +++++++++++++++++++++---------- 1 file changed, 155 insertions(+), 67 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 3fbe147..60f8441 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -6,7 +6,7 @@ import uuid import math -from pydnp3 import opendnp3 +from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 from typing import List, Dict, Union, Any from dnp3.outstation import DNP3Outstation from dnp3.points import ( @@ -101,7 +101,8 @@ def on_message(self, simulation_id,message): angle2 = math.radians(m.get("angle")) #point.magnitude = math.cos(angle1) * m.get("magnitude") self.outstation.apply_update(opendnp3.Analog(angle2), point.index) - + + elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): @@ -110,6 +111,71 @@ def on_message(self, simulation_id,message): except Exception as e: message_str = "An error occurred while trying to translate the message received" + str(e) + def create_message_updates(self, simulation_id, message): + """ This method creates an atomic "updates" object for any outstation to consume via their .Apply method. + ---------- + headers: dict + A dictionary of headers that could be used to determine topic of origin and + other attributes. + message: object + + """ + try: + message_str = 'received message ' + str(message) + + builder = asiodnp3.UpdateBuilder() + json_msg = yaml.safe_load(str(message)) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + measurement_values = json_msg["message"]["measurements"] + + # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values + for y in measurement_values: + # print(self.processor_point_def.points_by_mrid()) + m = measurement_values[y] + if "magnitude" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + #print("point",point) + #print("y",y) + if m.get("measurement_mrid") == point.measurement_id and point.magnitude != float(m.get("magnitude")): + point.magnitude = float(m.get("magnitude")) + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + elif "VAR" in point.name: + angle = math.radians(m.get("angle")) + point.magnitude = math.sin(angle) * float(m.get("magnitude")) + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + elif "Watts" in point.name: + angle1 = math.radians(m.get("angle")) + point.magnitude = math.cos(angle1) * float(m.get("magnitude")) + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "angle" in point.name: + angle2 = math.radians(m.get("angle")) + #point.magnitude = math.cos(angle1) * m.get("magnitude") + builder.Update(opendnp3.Analog(angle2), point.index) + + # elif "Net" in point.name: + # if + + elif "value" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): + point.value = m.get("value") + builder.Update(opendnp3.Binary(point.value), point.index) + + print("Updates Created") + return builder.Build() + except Exception as e: + message_str = "An error occurred while trying to translate the message received" + str(e) + + def assign_val_a(self, data_type, group, variation, index, name, description, measurement_type, measurement_id): """ Method is to initialize parameters to be used for generating output points for measurement key values """ records = dict() # type: Dict[str, Any] @@ -219,7 +285,7 @@ def _create_dnp3_object_map(self): elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": - if "Reg" in m['name']: + if "RatioTapChanger" in m['name'] or "reg" in m["SimObject"]: for r in range(5, 7): self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) self.c_ao += 1 @@ -245,86 +311,108 @@ def _create_dnp3_object_map(self): self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) self.c_do += 1 - # for m in regulators: - # reg_attribute = attribute_map['regulators']['attribute'] - # # bank_phase = list(m['bankPhases']) - # for n in range(0, 4): - # measurement_id = m.get("mRID") - # name = uuid.uuid4().hex - # description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] - # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) - # self.c_ao += 1 - # for i in range(5, 7): - # for j in range(0, len(m['bankPhases'])): - # measurement_id = m.get("mRID")[j] - # name = uuid.uuid4().hex - # description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] - # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) - # self.c_ao += 1 - # - # for m in solarpanels: - # measurement_id = m.get("mRID") - # name = uuid.uuid4().hex - # description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id - # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) - # self.c_ao += 1 - - # for m in batteries: - # measurement_id = m.get("mRID") - # name = uuid.uuid4().hex - # description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" - # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) - # self.c_ao += 1 + for m in regulators: + reg_attribute = attribute_map['regulators']['attribute'] + # bank_phase = list(m['bankPhases']) + for n in range(0, 4): + measurement_id = m.get("mRID") + name = m['bankname'] + '-' + m['bankPhases'] + description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) + self.c_ao += 1 + for i in range(5, 7): + for j in range(0, len(m['bankPhases'])): + measurement_id = m.get("mRID")[j] + name = m['tankName'] + '-' + m['bankPhases'] [j] + description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) + self.c_ao += 1 + + for m in solarpanels: + for k in range(0, len(m['phases'])): + measurement_id = m.get("mRID") + name = "Solar" + m['name'] + '-' + m['phases'][k] + '-Watts-value' + description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, "PowerElectronicsConnection.p") + self.c_ao += 1 + + name1 = "Solar" + m['name'] + '-' + m['phases'][k] + '-VAR-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name1, description, measurement_id, "PowerElectronicsConnection.q") + self.c_ao += 1 + + name2 = "Solar" + m['name'] + '-' + m['phases'][k] + '-VAR-Net-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name2, description, measurement_id, "PowerElectronicsConnection.q") + self.c_ao += 1 + + name3 = "Solar"+ m['name'] + '-' + m['phases'][k] + '-Watts-Net-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name3, description, measurement_id, "PowerElectronicsConnection.p") + self.c_ao += 1 + + for m in batteries: + for l in range(0, len(m['phases'])): + measurement_id = m.get("mRID") + name = m['name'] + '-' + m['phases'][l] + '-Watts-value' + description = "Battery, " + m['name'][l] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" + self.assign_val_d("AO", 42, 3, self.c_ao, name, description,measurement_id, "PowerElectronicsConnection.p") + self.c_ao += 1 + name1 = m['name'] + '-' + m['phases'][l] + '-VAR-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name1, description,measurement_id, "PowerElectronicsConnection.q") + self.c_ao += 1 + for m in switches: measurement_id = m.get("mRID") switch_attribute = attribute_map['switches']['attribute'] for k in range(0, len(m['phases'])): phase_value = list(m['phases']) - name = m['name'] + name = m['name'] + "Phase:" + m['phases'][k] description = "Name:" + m["name"] + ",ConductingEquipment_type:LoadBreakSwitch" + "Phase:" + phase_value[k] +",controlAttribute:"+switch_attribute self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 - # for m in fuses: - # measurement_id = m.get("mRID") - # switch_attribute = attribute_map['switches']['attribute'] - # for l in range(0, len(m['phases'])): - # phase_value = list(m['phases']) - # name = uuid.uuid4().hex - # description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id - # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - # self.c_do += 1 - - # for m in breakers: - # measurement_id = m.get("mRID") - # switch_attribute = attribute_map['switches']['attribute'] - # for n in range(0, len(m['phases'])): - # phase_value = list(m['phases']) - # name =uuid.uuid4().hex - # description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute - # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - # self.c_do += 1 - # - # for m in reclosers: - # measurement_id = m.get("mRID") - # switch_attribute = attribute_map['switches']['attribute'] - # for k in range(0, len(m['phases'])): - # phase_value = list(m['phases']) - # name = uuid.uuid4().hex - # description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute - # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - # self.c_do += 1 + for m in fuses: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for l in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "Phase:" + m['phases'][l] + description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + for m in breakers: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for n in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "Phase:" + m['phases'][n] + description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + for m in reclosers: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for i in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "Phase:" + m['phases'][i] + description = "Recloser, " + m["name"] + "Phase: - " + phase_value[i] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 for m in energyconsumers: measurement_id = m.get("mRID") for k in range(0, len(m['phases'])): phase_value = list(m['phases']) - name = m['name'] - description = "EnergyConsumer, " + m["name"] + "Phase: - " + phase_value[k] - self.assign_val_a("AI", 30, 1, self.c_ai, name, description, None , measurement_id) - self.c_ai += 1 + name = m['name']+"phase:" + m['phases'][k] + description = "EnergyConsumer, " + m["name"] + "Phase: " + phase_value[k] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, "EnergyConsumer.p") + self.c_ao += 1 + + name1 = m['name']+"phase:" + m['phases'][k] + "control" + self.assign_val_d("DO", 12, 1, self.c_do, name1, description, measurement_id, "EnergyConsumer.p") + self.c_do += 1 return self.out_json From dcdfd16389abb666d9630a795f4a831c071bdd52 Mon Sep 17 00:00:00 2001 From: singha42 Date: Wed, 8 Jul 2020 13:35:20 -0400 Subject: [PATCH 14/64] old-cim_to_dnp3.py --- dnp3/service/dnp3/cim_to_dnp3.py-8-Jul | 330 +++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100755 dnp3/service/dnp3/cim_to_dnp3.py-8-Jul diff --git a/dnp3/service/dnp3/cim_to_dnp3.py-8-Jul b/dnp3/service/dnp3/cim_to_dnp3.py-8-Jul new file mode 100755 index 0000000..3fbe147 --- /dev/null +++ b/dnp3/service/dnp3/cim_to_dnp3.py-8-Jul @@ -0,0 +1,330 @@ +import json +import yaml +import sys +import datetime +import random +import uuid +import math + +from pydnp3 import opendnp3 +from typing import List, Dict, Union, Any +from dnp3.outstation import DNP3Outstation +from dnp3.points import ( + PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT +) + +out_json = list() + +'''Dictionary for mapping the attribute values of control poitns for Capacitor, Regulator and Switches''' + +attribute_map = { + "capacitors": { + "attribute": ["RegulatingControl.mode", "RegulatingControl.targetDeadband", "RegulatingControl.targetValue", + "ShuntCompensator.aVRDelay", "ShuntCompensator.sections"]} + , + "switches": { + "attribute": "Switch.open" + } + , + + "regulators": { + "attribute": ["RegulatingControl.targetDeadband", "RegulatingControl.targetValue", "TapChanger.initialDelay", + "TapChanger.lineDropCompensation", "TapChanger.step", "TapChanger.lineDropR", + "TapChanger.lineDropX"]} + +} + + +class DNP3Mapping(): + """ This creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" + + def __init__(self, map_file): + self.c_ao = 0 + self.c_do = 0 + self.c_ai = 0 + self.c_di = 0 + self.c_var = 0 + self.measurements = dict() + self.out_json = list() + self.file_dict = map_file + self.processor_point_def = PointDefinitions() + self.outstation = DNP3Outstation('',0,'') + + + def on_message(self, simulation_id,message): + """ This method handles incoming messages on the fncs_output_topic for the simulation_id. + Parameters + ---------- + headers: dict + A dictionary of headers that could be used to determine topic of origin and + other attributes. + message: object + + """ + + try: + message_str = 'received message ' + str(message) + + json_msg = yaml.safe_load(str(message)) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + measurement_values = json_msg["message"]["measurements"] + + # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values + for y in measurement_values: + # print(self.processor_point_def.points_by_mrid()) + m = measurement_values[y] + if "magnitude" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + #print("point",point) + #print("y",y) + if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): + point.magnitude = m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "VAR" in point.name: + angle = math.radians(m.get("angle")) + point.magnitude = math.sin(angle) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "Watts" in point.name: + angle1 = math.radians(m.get("angle")) + point.magnitude = math.cos(angle1) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "angle" in point.name: + angle2 = math.radians(m.get("angle")) + #point.magnitude = math.cos(angle1) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(angle2), point.index) + + elif "value" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): + point.value = m.get("value") + self.outstation.apply_update(opendnp3.Binary(point.value), point.index) + except Exception as e: + message_str = "An error occurred while trying to translate the message received" + str(e) + + def assign_val_a(self, data_type, group, variation, index, name, description, measurement_type, measurement_id): + """ Method is to initialize parameters to be used for generating output points for measurement key values """ + records = dict() # type: Dict[str, Any] + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + records["measurement_type"] = measurement_type + records["measurement_id"] = measurement_id + records["magnitude"] = "0" + self.out_json.append(records) + + def assign_val_d(self, data_type, group, variation, index, name, description, measurement_id, attribute): + """ This method is to initialize parameters to be used for generating output points for output points""" + records = dict() + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + # records["measurement_type"] = measurement_type + records["measurement_id"] = measurement_id + records["attribute"] = attribute + records["value"] = "0" + self.out_json.append(records) + + def assign_valc(self, data_type, group, variation, index, name, description, measurement_id, attribute): + """ Method is to initialize parameters to be used for generating dnp3 control as Analog/Binary Input points""" + records = dict() + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + # records["measurement_type"] = measurement_type + records["attribute"] = attribute + records["measurement_id"] = measurement_id + self.out_json.append(records) + + def load_json(self, out_json, out_file): + with open(out_file, 'w') as fp: + out_dict = dict({'points': out_json}) + json.dump(out_dict, fp, indent=2, sort_keys=True) + + def load_point_def(self, point_def): + self.processor_point_def = point_def + + def load_outstation(self, outstation): + self.outstation = outstation + + def _create_dnp3_object_map(self): + """This method creates the points by taking the input data from model dictionary file""" + + feeders = self.file_dict.get("feeders", []) + measurements = list() + capacitors = list() + regulators = list() + switches = list() + solarpanels = list() + batteries = list() + fuses = list() + breakers = list() + reclosers = list() + energyconsumers = list() + for x in feeders: + measurements = x.get("measurements", []) + capacitors = x.get("capacitors", []) + regulators = x.get("regulators", []) + switches = x.get("switches", []) + solarpanels = x.get("solarpanels", []) + batteries = x.get("batteries", []) + fuses = x.get("fuses", []) + breakers = x.get("breakers", []) + reclosers = x.get("reclosers", []) + energyconsumers = x.get("energyconsumers", []) + + for m in measurements: + attribute = attribute_map['regulators']['attribute'] + measurement_type = m.get("measurementType") + measurement_id = m.get("mRID") + name= m['name'] + '-' + m['phases'] + description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + if m['MeasurementClass'] == "Analog": + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) + self.c_ai += 1 + + if m.get("measurementType") == "VA": + measurement_id = m.get("mRID") + name1 = m['name'] + '-' + m['phases'] + '-VAR-value' + name2 = m['name'] + '-' + m['phases'] + '-Watts-value' + name3 = m['name'] + '-' + m['phases'] + '-angle' + + description1 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "VAR" + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + description2 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "Watt" + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + description3 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "angle" + ",ConnectivityNode:" + m.get("ConnectivityNode") + ",SimObject:" + m.get("SimObject") + if m['MeasurementClass'] == "Analog": + self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) + self.c_ai += 1 + + + elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": + if "Reg" in m['name']: + for r in range(5, 7): + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) + self.c_ao += 1 + else: + self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) + self.c_di += 1 + + + for m in capacitors: + measurement_id = m.get("mRID") + cap_attribute = attribute_map['capacitors']['attribute'] # type: List[str] + + for l in range(0, 4): + # publishing attribute value for capacitors as Bianry/Analog Input points based on phase attribute + name = m['name'] + description = "Name:" + m['name'] + "ConductingEquipment_type:LinearShuntCompensator" + ",Attribute:" + cap_attribute[l] + ",Phase:" + m['phases'] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, cap_attribute[l]) + self.c_ao += 1 + for p in range(0, len(m['phases'])): + name = m['name'] + m['phases'][p] + description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[p] + ",Phase:" + m['phases'][p] + # description = "Capacitor, " + m['name'] + "," + "phase -" + m['phases'][p] + ", and attribute is - " + cap_attribute[4] + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) + self.c_do += 1 + + # for m in regulators: + # reg_attribute = attribute_map['regulators']['attribute'] + # # bank_phase = list(m['bankPhases']) + # for n in range(0, 4): + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] + # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) + # self.c_ao += 1 + # for i in range(5, 7): + # for j in range(0, len(m['bankPhases'])): + # measurement_id = m.get("mRID")[j] + # name = uuid.uuid4().hex + # description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] + # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) + # self.c_ao += 1 + # + # for m in solarpanels: + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id + # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + # self.c_ao += 1 + + # for m in batteries: + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" + # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + # self.c_ao += 1 + + for m in switches: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for k in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "Name:" + m["name"] + ",ConductingEquipment_type:LoadBreakSwitch" + "Phase:" + phase_value[k] +",controlAttribute:"+switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + # for m in fuses: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for l in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name = uuid.uuid4().hex + # description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + + # for m in breakers: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for n in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name =uuid.uuid4().hex + # description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + # + # for m in reclosers: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for k in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name = uuid.uuid4().hex + # description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + + + for m in energyconsumers: + measurement_id = m.get("mRID") + for k in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "EnergyConsumer, " + m["name"] + "Phase: - " + phase_value[k] + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, None , measurement_id) + self.c_ai += 1 + + return self.out_json + From 35a85e7505e755a77de565ec25344eb70ccaba57 Mon Sep 17 00:00:00 2001 From: Seth Gossler Date: Thu, 16 Jul 2020 11:28:35 -0700 Subject: [PATCH 15/64] Update - new branch that holds the latest net value measurements as well as some fixes. --- .../dnp3/__pycache__/__init__.cpython-37.pyc | Bin 136 -> 146 bytes .../__pycache__/cim_to_dnp3.cpython-37.pyc | Bin 7734 -> 13318 bytes .../__pycache__/outstation.cpython-37.pyc | Bin 15274 -> 15364 bytes .../dnp3/__pycache__/points.cpython-37.pyc | Bin 27747 -> 28299 bytes dnp3/service/dnp3/cim_to_dnp3.py | 283 +++++++++++---- dnp3/service/dnp3/cim_to_dnp3.py-8-Jul | 330 ++++++++++++++++++ dnp3/service/dnp3/outstation.py | 21 ++ dnp3/service/new_start_service.py | 20 +- 8 files changed, 578 insertions(+), 76 deletions(-) create mode 100755 dnp3/service/dnp3/cim_to_dnp3.py-8-Jul diff --git a/dnp3/service/dnp3/__pycache__/__init__.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/__init__.cpython-37.pyc index 96d22a14d5dc6683728515c127db0b3adc767d31..ba067d095495a8dda9a11923dfaa3db965905f8d 100644 GIT binary patch delta 55 zcmeBRoW#iE#LLUY00eT+XT(k9F_*X2PcO<$Nh~NRPSGz;Eh@`QPAx_V=}wF=005?k B5o-Vd delta 45 zcmbQl*ulu-#LLUY00eLEGR02hF&8z|PcO<$Nh~NRPSGz;Eh@`QPA%4-m}CF|4QCCc diff --git a/dnp3/service/dnp3/__pycache__/cim_to_dnp3.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/cim_to_dnp3.cpython-37.pyc index a11f2d60d8e8fb74084f298c2a130c16e2b05279..940573fcd9a0870d2897434141a117eec56fd94f 100644 GIT binary patch literal 13318 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 diff --git a/dnp3/service/dnp3/__pycache__/outstation.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/outstation.cpython-37.pyc index 3e599e68de23da5c6b799531e3c019853d08ea60..3a632f70674a595ad34afcc098359bf24768f302 100644 GIT binary patch delta 5041 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 z z-$V5+AgRbazA-$=53ro@Mxz>4EvWO{%$6+WOu0E(0<= z!tf*Pn;;g85mF?R@u;Y~t`9EHOJH|b&DeR1|9N_sehmm&5BJ@@-^Rdm!*lhIQgk}E zaSp8Y5ag{alzibzDB(7%3)L{PUE`h{+$OT-%pi>HE&%vQG~i+AS0d}ux-=iTGtwG~ I8Ucg+AC84D;Q#;t delta 5183 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 TN>+r5kV@`NCX*d%n@au-7Hz10 diff --git a/dnp3/service/dnp3/__pycache__/points.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/points.cpython-37.pyc index 2769fa2baf8bf3965908a5e66a725ea99c11c29d..053d896de175b29f21d56db12108eb9f9085ea36 100644 GIT binary patch delta 4194 zcma)8Yj9J?71mxokY!nxY~v@`vWaa325i|FKOnY|6^vuB4aO--iYohFKt`6#N+#IY z7SB9~&~TcRK+@Pv8k&%lq&G>E$pk`ZJcZB{=u8rB2Bw|1)5oNpPG{QuNon`&m1OZs z%j45IyXW!k*|U51cb}8TuM*43v^0~3{VDw8kxlOB-mn}tl6sgjW|_87zvvqp3X6;2 zEn{28PneCQ@eont5k%tAiVjFm@z*a9=Zb~-9Cd5Wc##YyNwM&$u(xl7=X#p3wOB1|Mk5$VHn9*RF=!#JxJZXo;LEZKR-nA{ zO7bGKmsg-}cmg9Hg5qU)a}|p(5J;ka9K#71jf@t-rLuf0&*C6!WB+nsPx*@K-53Ek zita<$kMJ znMr0-p2=pM#pbkf<@`=U?6BK$bfIoa7}I#oV`P$u7BQV`X#`qI?0!eJ$sxE|)LpPHS;k z)MDLKr$nItI}VludTYwaKjrb7FNk0hz*GBM{Ru{9E;KJYZ%`R<#*VrIQUdO}zmRgd zvObffRJ6H?0#d)x$;i%z3tjZ2Nm`Hsa6YzF98MBfSR;}DB zc=uWI8aSJluuHA4$+t?~D0F)P;W)y}2zd8WTq-?@z%PcMq9h|AUHTTn&k)`LQ}Y7- zVJ5x;_05aOVfcG1)>w23WkO#;mj(p9NoX5`4e)Bq$GR7p1dA*4T#i}v zeVmdIIHx|8?nCHAz|+7Y;hlABKy>iHXDi#6proXEz5M}SINUF>E5kHsB13Cy%k z?s9bt`8@L$rgLU$IZvbd0lep)OOC@8cR|)|)*#Bpdw{ghxZjC5Rim+x-?)VB@P}37 zdfcF0u%&G~KV4C{+V;+h571SomWUJOJH|)VBTRpc7PnLy&Gy0N)$(jSsp|ECUeu3p znm3x93f7O7asZhz_R8sGA%!|=6%7y>o*M#bXAhmaIh=45#u`y>i6kG4D<_x za|q`VK4uUPd1F_-ApfRom#|Tdh^CzCZCdH(ko;ne#l3Tny$x@!KfpI=8t(2HKXn0oD zX#6_iwcQ0J8}HKN9rI_ncjL3-Dl5OIc=z!<7xDX^H-hfCY7-N1)$D_wV%F0Un7CiG<+S=DKKs^CK$NI` zbBNmIf8YO!K+eL2O_hRH3)eRdlFRHc4B8I<)lH@TISURInk3 z10Pxap~0cMS&#CX;ptWk;Sh~TBE8HM{=%Xk!L~kU`e&%~#>Yk|gjf4sGU7SGD-y0{ z=UOfO>~ZSz2g6=}P?UBqfxZ0~%lL7fX&MX~f3Za|AkTXac8c>!HB5M=s-!}40lK8RsA6edz0U3G@pg8(*R}Mx6~lUW2h){V zu>txzhI|m=4TK#CKVqPmy72Ak!pl}M@Sm#KuQT>rP2WVr+3O?h%hkWqAN0|Y-uQP+ zF|DGZ=!-oDm#@d_YU@0y`A~+kTb~|WoOJ2CMh(^@z5w&D{yLaiA70eoEgnZJ`I1{TpW!LsmqAXqvBMuRW`2BaFE@n zY)Jip$2^=Eq$*i)CM!#mm0D$v8tWb!8kK@c5u^M~5VuVx;Z#YejJT2ri8HmyKJ|*3 zFN~X2RVuT2d6LrHkVV}CHE^+0-Gx{hR1#@Wsa0U{);WnGtbSt6>Q=-&QMtj4c}BNp z$Lm|2TrIA|tHqV-*lM^^ldQyJ@|oILZ?yz{fzTF~@rE6#PF@&SoXQ539@i2{&@PcA zdCA-?URkazi*rG9J0#Qd>!Z zV*>^A5@RZ1wb!cD&Y}Dj`z~A=sLaPFrTPlQcLL>KP`upPjdfkjksG$nA!+wnqZ)@b L-P&Yru$KK7l#rn- delta 3709 zcma)8eNa@_73VIy%YLyg1cc?AsK^q5Wg(~-q6=d`~m`i$8uya#MqcFEmEW5I3` zYtk{Om19zkiGe0IHEE;fB@;j5NJ`U*+UaL%?Pi*3l1XOjOght<&b0rv_uTh(ePyJr zIKOw!z32YU@0@$@d;9F4$mchSy(Kf#lE(g-YKnKBc;TFV#zbmi&XfaJOzqhx*@ToP z5HU^Ai9|>jSAo+Uv{s14V##-ikR`5$gJ!2e*O%t1lrNb_96BKn&gJJ3L2fRv5klmS z!V|=>i_zRK-zs{8kal>!WPv;hLTM-2D!*2mO-LLrmKBpH;AYu6vQ5q^*XziBSiV9e zgYx8xMjd$u-deSmOu?;HH(1pB6?xFJXQPa*6_cn0ACgl{4|J5yC&xsq8XqSskWCcVjS$~5Jf%*;wRIZYOm zjagQU(~@P%khfHI6Os)t-Sfgd_Bq{Dn%_1>W{J2=)NwD9dk!&+dv@++i+UJX(`gj- ztXKKa+1d~J)gDK}J|+dC6I2?L!ZCkPgzoAR{o_mrOjRF_=O)slF-k>L*nw~m!HAH7 zFph8t;V{DY5RM=q1D!ztgrf-0Av}-peS~8O#}Nh?xOCLV9to=yQlyB|5QW1vca@$X zX=%|4HkYQ$CicaD>99~UNo3eq8!|l2V%~+bwSMD^%%V@jkyU@SuY1EI*^s49xD2{$;%6*=B;fW&zXD8Gx#REp&_Eme-ouC!`V zmr_LkHxG6MzS~$qK9|oleoS=#A~3z~Q^P4H#tL-(HKU?{E8fvmNF1=c=@!X_a~sRx zqSpe`8~#EHVAp+@jT6|87zWxBW>$G*A~rq|D}_gzd&o&R+gxs#Wf3dkM)POvD6=qg z!YB=f0?}wCe0={Iatg;X6PD<%01XCW!AQ88WjSd*i@f}BoK_>GRD_4?nBBBMUIWov zOV+|sZ(y^sF}Um{ggJy)5PpP!rP0#}5riKj`~+bZ;S9n#gty?nmMX(EGmpV&OEsC6 zkGGVN)ySrvBYFaDLSM!3CWH+L?F?Gslh*5ce6hfz9;e-vLEptG31JRCYAerKv4L$d*!BfARV`t^Jd{)p${Q+Bk)V9urZfO@F|Zwl;DTFj=culfFK<2c#PkcFgc3By~8I zFiYWyF^L9Z5=GgG951DTJr72L;aHnA6buLPbkR#lun$_gddNOF+7)c&wZDU-w@(mP zc^St>5Y8hsCE-7I1B>d0P2C5`ynMa;vd+kxIuD0>-Q)sX>CNAO{4OK!9(@lveu}Vw z@H2!D7$hlqj?{5FB4_ta>b5J47`o&(Eu!S_PL+4*U3(S2{KkI1Yj4AN{}|u3^YB^! z&7MW#jLQDaqXgcWMf3l2C*K3>w%+a~8~tC}i#ZSJ*jw7h4E%oC?y@;-dYhikLwA)E zKG?Q_?3T&)EV6MA%9}#qUx*M|yb+VkVpkCxL6;7K9wCohhoJC_Qof!u@jJkoPNnGf zz}6ir4fs;|1MJ=LBKb2EJp6vq?KSCaMzFRvXMwglxW#dgq8)$w)pzI)xN$y zdDSDo)RD{Z_eb5jzv-YNFh;Jin^C~UnPoZf2_ehDE4tv)V2=qEy3*-&$Pw4Dm$z4J z%IDX_7>&@FB+_drZ6CrF0MWDTS7`FH_zJUZ@R9gKmTOX~A;s{z^y>!=tVta%f-!AsE=1$=g;~q^k z+8%W!+VukL9VyzPj@0wn0?ySInHUI3cV=wRFruAgM4K^wIdXT7U}aL7Iyav#2@US|!kju+hN}tkal^C6PN+5cvH0U%6g`DeaH1aJeyc8quN2+3F zb5weCQiSL!q6TeA+(~BRPBI&J8U*-sv^=Pi zHE4x$C$&FY06hj)jVH)dN+hoG3xLYVdL> zcSUMO(WV@{IuCjt%71^#K*rn!)A%xx5cn^cd9?7IO|Q;W<;eqMMI_Vbh^M(6%N!dW IO^%9x1L-0B-~a#s diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 3fbe147..8c78008 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -5,8 +5,10 @@ import random import uuid import math +# import pydevd;pydevd.settrace(suspend=False) # Uncomment For Debugging on other Threads -from pydnp3 import opendnp3 +from collections import defaultdict +from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 from typing import List, Dict, Union, Any from dnp3.outstation import DNP3Outstation from dnp3.points import ( @@ -50,7 +52,7 @@ def __init__(self, map_file): self.processor_point_def = PointDefinitions() self.outstation = DNP3Outstation('',0,'') - + # This is the old on_message. Should probably be removed, unless the "start_service.py" needs it. Leaving this up to PNNL to decide :) def on_message(self, simulation_id,message): """ This method handles incoming messages on the fncs_output_topic for the simulation_id. Parameters @@ -101,7 +103,8 @@ def on_message(self, simulation_id,message): angle2 = math.radians(m.get("angle")) #point.magnitude = math.cos(angle1) * m.get("magnitude") self.outstation.apply_update(opendnp3.Analog(angle2), point.index) - + + elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): @@ -110,6 +113,95 @@ def on_message(self, simulation_id,message): except Exception as e: message_str = "An error occurred while trying to translate the message received" + str(e) + # create_message_updates is called in new_start_service.py (in the new on_message method). + def create_message_updates(self, simulation_id, message): + """ This method creates an atomic "updates" object for any outstation to consume via their .Apply method. + ---------- + headers: dict + A dictionary of headers that could be used to determine topic of origin and + other attributes. + message: object + + """ + try: + message_str = 'received message ' + str(message) + + builder = asiodnp3.UpdateBuilder() + json_msg = yaml.safe_load(str(message)) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + measurement_values = json_msg["message"]["measurements"] + + # Calculate each net-phase measurement + if(measurement_values): + + myPoints = self.outstation.get_agent().point_definitions.all_points() + netPoints = list(filter(lambda x: "net-" in x.description, myPoints)) + + for point in netPoints: + ptMeasurements = list(filter(lambda m: m.get("measurement_mrid") in point.measurement_id, measurement_values.values())) + netValue = 0.0 + + for m in ptMeasurements: + if "VAR" in point.name: + angle = math.radians(m.get("angle")) + netValue = netValue + math.sin(angle) * float(m.get("magnitude")) + + elif "Watts" in point.name: + angle = math.radians(m.get("angle")) + netValue += math.cos(angle) * float(m.get("magnitude")) + + else: + netValue += float(m.get("magnitude")) + + point.magnitude = netValue + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + # Calculate each measurement + for y in measurement_values: + # print(self.processor_point_def.points_by_mrid()) + m = measurement_values[y] + + if "magnitude" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + #print("point",point.name) + #print("y",y) + if m.get("measurement_mrid") == point.measurement_id: + if point.magnitude != float(m.get("magnitude")): + point.magnitude = float(m.get("magnitude")) + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + if point.measurement_type == "VA": + if "VAR" in point.name: + angle = math.radians(m.get("angle")) + point.magnitude = math.sin(angle) * float(m.get("magnitude")) + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + if "Watts" in point.name: + angle1 = math.radians(m.get("angle")) + point.magnitude = math.cos(angle1) * float(m.get("magnitude")) + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + if "angle" in point.name: + angle2 = math.radians(m.get("angle")) + builder.Update(opendnp3.Analog(angle2), point.index) + + elif "value" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): + point.value = m.get("value") + builder.Update(opendnp3.Binary(point.value), point.index) + + # Return the atomic "updates" object + return builder.Build() + except Exception as e: + message_str = "An error occurred while trying to translate the message received" + str(e) + def assign_val_a(self, data_type, group, variation, index, name, description, measurement_type, measurement_id): """ Method is to initialize parameters to be used for generating output points for measurement key values """ records = dict() # type: Dict[str, Any] @@ -190,6 +282,35 @@ def _create_dnp3_object_map(self): reclosers = x.get("reclosers", []) energyconsumers = x.get("energyconsumers", []) + # Unique grouping of measurements - GroupBy Name, Type and Connectivity node + groupByNameTypeConNode = defaultdict(list) + for m in measurements: + groupByNameTypeConNode[m['name']+m.get("measurementType")+m.get("ConnectivityNode")].append(m) + + # Create Net Phase DNP3 Points + for grpM in groupByNameTypeConNode.values(): + + if grpM[0]['MeasurementClass'] == "Analog" and grpM[0].get("measurementType") == "VA": + measurement_type = grpM[0].get("measurementType") + measurement_id = grpM[0].get("mRID") +","+ grpM[1].get("mRID") +","+ grpM[2].get("mRID") + + + name1 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VAR-value' + name2 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-Watts-value' + name3 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VA-value' + + description1 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VAR" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description2 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-Watts" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description3 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VA" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + + self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) + self.c_ai += 1 + + # Create Each Phase DNP3 Points for m in measurements: attribute = attribute_map['regulators']['attribute'] measurement_type = m.get("measurementType") @@ -219,15 +340,15 @@ def _create_dnp3_object_map(self): elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": - if "Reg" in m['name']: - for r in range(5, 7): + if "RatioTapChanger" in m['name'] or "reg" in m["SimObject"]: + # TODO: Do we need step? + for r in range(5, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) self.c_ao += 1 else: self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) self.c_di += 1 - for m in capacitors: measurement_id = m.get("mRID") cap_attribute = attribute_map['capacitors']['attribute'] # type: List[str] @@ -245,86 +366,106 @@ def _create_dnp3_object_map(self): self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) self.c_do += 1 - # for m in regulators: - # reg_attribute = attribute_map['regulators']['attribute'] - # # bank_phase = list(m['bankPhases']) - # for n in range(0, 4): - # measurement_id = m.get("mRID") - # name = uuid.uuid4().hex - # description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] - # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) - # self.c_ao += 1 - # for i in range(5, 7): - # for j in range(0, len(m['bankPhases'])): - # measurement_id = m.get("mRID")[j] - # name = uuid.uuid4().hex - # description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] - # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) - # self.c_ao += 1 - # - # for m in solarpanels: - # measurement_id = m.get("mRID") - # name = uuid.uuid4().hex - # description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id - # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) - # self.c_ao += 1 - - # for m in batteries: - # measurement_id = m.get("mRID") - # name = uuid.uuid4().hex - # description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" - # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) - # self.c_ao += 1 - + for m in regulators: + reg_attribute = attribute_map['regulators']['attribute'] + # bank_phase = list(m['bankPhases']) + for n in range(0, 4): + measurement_id = m.get("mRID") + name = m['bankName'] + '-' + m['bankPhases'] + description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) + self.c_ao += 1 + for i in range(5, 7): + for j in range(0, len(m['bankPhases'])): + measurement_id = m.get("mRID")[j] + name = m['tankName'][j] + '-' + m['bankPhases'][j] + description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) + self.c_ao += 1 + + for m in solarpanels: + for k in range(0, len(m['phases'])): + measurement_id = m.get("mRID") + name = "Solar" + m['name'] + '-' + m['phases'][k] + '-Watts-value' + description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, "PowerElectronicsConnection.p") + self.c_ao += 1 + + name1 = "Solar" + m['name'] + '-' + m['phases'][k] + '-VAR-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name1, description, measurement_id, "PowerElectronicsConnection.q") + self.c_ao += 1 + + name2 = "Solar" + m['name'] + '-' + m['phases'][k] + '-VAR-Net-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name2, description, measurement_id, "PowerElectronicsConnection.q") + self.c_ao += 1 + + name3 = "Solar"+ m['name'] + '-' + m['phases'][k] + '-Watts-Net-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name3, description, measurement_id, "PowerElectronicsConnection.p") + self.c_ao += 1 + + for m in batteries: + for l in range(0, len(m['phases'])): + measurement_id = m.get("mRID") + name = m['name'] + '-' + m['phases'][l] + '-Watts-value' + description = "Battery, " + m['name'][l] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" + self.assign_val_d("AO", 42, 3, self.c_ao, name, description,measurement_id, "PowerElectronicsConnection.p") + self.c_ao += 1 + name1 = m['name'] + '-' + m['phases'][l] + '-VAR-value' + self.assign_val_d("AO", 42, 3, self.c_ao, name1, description,measurement_id, "PowerElectronicsConnection.q") + self.c_ao += 1 + for m in switches: measurement_id = m.get("mRID") switch_attribute = attribute_map['switches']['attribute'] for k in range(0, len(m['phases'])): phase_value = list(m['phases']) - name = m['name'] + name = m['name'] + "Phase:" + m['phases'][k] description = "Name:" + m["name"] + ",ConductingEquipment_type:LoadBreakSwitch" + "Phase:" + phase_value[k] +",controlAttribute:"+switch_attribute self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 - # for m in fuses: - # measurement_id = m.get("mRID") - # switch_attribute = attribute_map['switches']['attribute'] - # for l in range(0, len(m['phases'])): - # phase_value = list(m['phases']) - # name = uuid.uuid4().hex - # description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id - # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - # self.c_do += 1 - - # for m in breakers: - # measurement_id = m.get("mRID") - # switch_attribute = attribute_map['switches']['attribute'] - # for n in range(0, len(m['phases'])): - # phase_value = list(m['phases']) - # name =uuid.uuid4().hex - # description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute - # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - # self.c_do += 1 - # - # for m in reclosers: - # measurement_id = m.get("mRID") - # switch_attribute = attribute_map['switches']['attribute'] - # for k in range(0, len(m['phases'])): - # phase_value = list(m['phases']) - # name = uuid.uuid4().hex - # description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute - # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) - # self.c_do += 1 + for m in fuses: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for l in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "Phase:" + m['phases'][l] + description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + for m in breakers: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for n in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "Phase:" + m['phases'][n] + description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + for m in reclosers: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for i in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + "Phase:" + m['phases'][i] + description = "Recloser, " + m["name"] + "Phase: - " + phase_value[i] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 for m in energyconsumers: measurement_id = m.get("mRID") for k in range(0, len(m['phases'])): phase_value = list(m['phases']) - name = m['name'] - description = "EnergyConsumer, " + m["name"] + "Phase: - " + phase_value[k] - self.assign_val_a("AI", 30, 1, self.c_ai, name, description, None , measurement_id) - self.c_ai += 1 + name = m['name']+"phase:" + m['phases'][k] + description = "EnergyConsumer, " + m["name"] + "Phase: " + phase_value[k] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, "EnergyConsumer.p") + self.c_ao += 1 + + name1 = m['name']+"phase:" + m['phases'][k] + "control" + self.assign_val_d("DO", 12, 1, self.c_do, name1, description, measurement_id, "EnergyConsumer.p") + self.c_do += 1 return self.out_json diff --git a/dnp3/service/dnp3/cim_to_dnp3.py-8-Jul b/dnp3/service/dnp3/cim_to_dnp3.py-8-Jul new file mode 100755 index 0000000..3fbe147 --- /dev/null +++ b/dnp3/service/dnp3/cim_to_dnp3.py-8-Jul @@ -0,0 +1,330 @@ +import json +import yaml +import sys +import datetime +import random +import uuid +import math + +from pydnp3 import opendnp3 +from typing import List, Dict, Union, Any +from dnp3.outstation import DNP3Outstation +from dnp3.points import ( + PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT +) + +out_json = list() + +'''Dictionary for mapping the attribute values of control poitns for Capacitor, Regulator and Switches''' + +attribute_map = { + "capacitors": { + "attribute": ["RegulatingControl.mode", "RegulatingControl.targetDeadband", "RegulatingControl.targetValue", + "ShuntCompensator.aVRDelay", "ShuntCompensator.sections"]} + , + "switches": { + "attribute": "Switch.open" + } + , + + "regulators": { + "attribute": ["RegulatingControl.targetDeadband", "RegulatingControl.targetValue", "TapChanger.initialDelay", + "TapChanger.lineDropCompensation", "TapChanger.step", "TapChanger.lineDropR", + "TapChanger.lineDropX"]} + +} + + +class DNP3Mapping(): + """ This creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" + + def __init__(self, map_file): + self.c_ao = 0 + self.c_do = 0 + self.c_ai = 0 + self.c_di = 0 + self.c_var = 0 + self.measurements = dict() + self.out_json = list() + self.file_dict = map_file + self.processor_point_def = PointDefinitions() + self.outstation = DNP3Outstation('',0,'') + + + def on_message(self, simulation_id,message): + """ This method handles incoming messages on the fncs_output_topic for the simulation_id. + Parameters + ---------- + headers: dict + A dictionary of headers that could be used to determine topic of origin and + other attributes. + message: object + + """ + + try: + message_str = 'received message ' + str(message) + + json_msg = yaml.safe_load(str(message)) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + measurement_values = json_msg["message"]["measurements"] + + # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values + for y in measurement_values: + # print(self.processor_point_def.points_by_mrid()) + m = measurement_values[y] + if "magnitude" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + #print("point",point) + #print("y",y) + if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): + point.magnitude = m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "VAR" in point.name: + angle = math.radians(m.get("angle")) + point.magnitude = math.sin(angle) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "Watts" in point.name: + angle1 = math.radians(m.get("angle")) + point.magnitude = math.cos(angle1) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) + + elif point.measurement_type == "VA" and "angle" in point.name: + angle2 = math.radians(m.get("angle")) + #point.magnitude = math.cos(angle1) * m.get("magnitude") + self.outstation.apply_update(opendnp3.Analog(angle2), point.index) + + elif "value" in m.keys(): + for point in self.outstation.get_agent().point_definitions.all_points(): + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): + point.value = m.get("value") + self.outstation.apply_update(opendnp3.Binary(point.value), point.index) + except Exception as e: + message_str = "An error occurred while trying to translate the message received" + str(e) + + def assign_val_a(self, data_type, group, variation, index, name, description, measurement_type, measurement_id): + """ Method is to initialize parameters to be used for generating output points for measurement key values """ + records = dict() # type: Dict[str, Any] + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + records["measurement_type"] = measurement_type + records["measurement_id"] = measurement_id + records["magnitude"] = "0" + self.out_json.append(records) + + def assign_val_d(self, data_type, group, variation, index, name, description, measurement_id, attribute): + """ This method is to initialize parameters to be used for generating output points for output points""" + records = dict() + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + # records["measurement_type"] = measurement_type + records["measurement_id"] = measurement_id + records["attribute"] = attribute + records["value"] = "0" + self.out_json.append(records) + + def assign_valc(self, data_type, group, variation, index, name, description, measurement_id, attribute): + """ Method is to initialize parameters to be used for generating dnp3 control as Analog/Binary Input points""" + records = dict() + records["data_type"] = data_type + records["index"] = index + records["group"] = group + records["variation"] = variation + records["description"] = description + records["name"] = name + # records["measurement_type"] = measurement_type + records["attribute"] = attribute + records["measurement_id"] = measurement_id + self.out_json.append(records) + + def load_json(self, out_json, out_file): + with open(out_file, 'w') as fp: + out_dict = dict({'points': out_json}) + json.dump(out_dict, fp, indent=2, sort_keys=True) + + def load_point_def(self, point_def): + self.processor_point_def = point_def + + def load_outstation(self, outstation): + self.outstation = outstation + + def _create_dnp3_object_map(self): + """This method creates the points by taking the input data from model dictionary file""" + + feeders = self.file_dict.get("feeders", []) + measurements = list() + capacitors = list() + regulators = list() + switches = list() + solarpanels = list() + batteries = list() + fuses = list() + breakers = list() + reclosers = list() + energyconsumers = list() + for x in feeders: + measurements = x.get("measurements", []) + capacitors = x.get("capacitors", []) + regulators = x.get("regulators", []) + switches = x.get("switches", []) + solarpanels = x.get("solarpanels", []) + batteries = x.get("batteries", []) + fuses = x.get("fuses", []) + breakers = x.get("breakers", []) + reclosers = x.get("reclosers", []) + energyconsumers = x.get("energyconsumers", []) + + for m in measurements: + attribute = attribute_map['regulators']['attribute'] + measurement_type = m.get("measurementType") + measurement_id = m.get("mRID") + name= m['name'] + '-' + m['phases'] + description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + measurement_type + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + if m['MeasurementClass'] == "Analog": + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, measurement_type, measurement_id) + self.c_ai += 1 + + if m.get("measurementType") == "VA": + measurement_id = m.get("mRID") + name1 = m['name'] + '-' + m['phases'] + '-VAR-value' + name2 = m['name'] + '-' + m['phases'] + '-Watts-value' + name3 = m['name'] + '-' + m['phases'] + '-angle' + + description1 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "VAR" + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + description2 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "Watt" + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + description3 = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + "angle" + ",ConnectivityNode:" + m.get("ConnectivityNode") + ",SimObject:" + m.get("SimObject") + if m['MeasurementClass'] == "Analog": + self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) + self.c_ai += 1 + + + elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": + if "Reg" in m['name']: + for r in range(5, 7): + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) + self.c_ao += 1 + else: + self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) + self.c_di += 1 + + + for m in capacitors: + measurement_id = m.get("mRID") + cap_attribute = attribute_map['capacitors']['attribute'] # type: List[str] + + for l in range(0, 4): + # publishing attribute value for capacitors as Bianry/Analog Input points based on phase attribute + name = m['name'] + description = "Name:" + m['name'] + "ConductingEquipment_type:LinearShuntCompensator" + ",Attribute:" + cap_attribute[l] + ",Phase:" + m['phases'] + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, cap_attribute[l]) + self.c_ao += 1 + for p in range(0, len(m['phases'])): + name = m['name'] + m['phases'][p] + description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[p] + ",Phase:" + m['phases'][p] + # description = "Capacitor, " + m['name'] + "," + "phase -" + m['phases'][p] + ", and attribute is - " + cap_attribute[4] + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) + self.c_do += 1 + + # for m in regulators: + # reg_attribute = attribute_map['regulators']['attribute'] + # # bank_phase = list(m['bankPhases']) + # for n in range(0, 4): + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] + # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) + # self.c_ao += 1 + # for i in range(5, 7): + # for j in range(0, len(m['bankPhases'])): + # measurement_id = m.get("mRID")[j] + # name = uuid.uuid4().hex + # description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] + # self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) + # self.c_ao += 1 + # + # for m in solarpanels: + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id + # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + # self.c_ao += 1 + + # for m in batteries: + # measurement_id = m.get("mRID") + # name = uuid.uuid4().hex + # description = "Battery, " + m['name'] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" + # self.assign_val_a("AO", 42, 3, self.c_ao, name, description, None, measurement_id) + # self.c_ao += 1 + + for m in switches: + measurement_id = m.get("mRID") + switch_attribute = attribute_map['switches']['attribute'] + for k in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "Name:" + m["name"] + ",ConductingEquipment_type:LoadBreakSwitch" + "Phase:" + phase_value[k] +",controlAttribute:"+switch_attribute + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + self.c_do += 1 + + # for m in fuses: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for l in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name = uuid.uuid4().hex + # description = "Name:" + m["name"] + ",Phase:" + phase_value[l] + ",Attribute:" + switch_attribute + ",mRID" + measurement_id + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + + # for m in breakers: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for n in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name =uuid.uuid4().hex + # description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + # + # for m in reclosers: + # measurement_id = m.get("mRID") + # switch_attribute = attribute_map['switches']['attribute'] + # for k in range(0, len(m['phases'])): + # phase_value = list(m['phases']) + # name = uuid.uuid4().hex + # description = "Recloser, " + m["name"] + "Phase: - " + phase_value[k] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute + # self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) + # self.c_do += 1 + + + for m in energyconsumers: + measurement_id = m.get("mRID") + for k in range(0, len(m['phases'])): + phase_value = list(m['phases']) + name = m['name'] + description = "EnergyConsumer, " + m["name"] + "Phase: - " + phase_value[k] + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, None , measurement_id) + self.c_ai += 1 + + return self.out_json + diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index 1ba3bed..1040927 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -334,6 +334,27 @@ def apply_update(self, value, index): if not os.environ.get('UNITTEST', False): raise err + def apply_compiled_updates(self, updates): + """ + Updates all records in the outstation's database with the atomic "updates" object. + + The data value gets sent to the Master as a side-effect. + + :param value: product of asiodnp3.UpdatBuilder.Build() + """ + #_log.debug('Recording DNP3 {} measurement, name={}, index={}, value={}'.format(type(value).__name__, value, index, value.value)) + + #max_index = cls.get_outstation_config().get('database_sizes', 10000) + #if index > max_index: + # raise ValueError('Attempt to set a value for index {} which exceeds database size {}'.format(index,max_index)) + try: + # print("Updating point values", self.port) + self.get_outstation().Apply(updates) + except AttributeError as err: + if not os.environ.get('UNITTEST', False): + raise err + print("error applied_compiled_updates:",err) + def shutdown(self): """ Execute an orderly shutdown of the Outstation. diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index 0106385..3a14ba7 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -54,7 +54,7 @@ def process_point_value(self, command_type, command, index, op_type): _log.debug("cmdtype={},command={},index={},optype={}".format(command_type, command, index, op_type)) point_value = self.point_definitions.point_value_for_command(command_type, command, index, op_type) """ Generating CIM messages for CROB and Analog type commands from Master. """ - + print("cmdtype={},command={},index={},optype={}".format(command_type, command, index, op_type)) if 'Control' in str(command): _log.debug("command_code={},command_code={},command_ontime={}".format(command.status, command.functionCode, command.onTimeMS)) for point in self.outstation.get_agent().point_definitions.all_points(): @@ -317,6 +317,18 @@ def load_point_definitions(self): def publish_outstation_status(status_string): print(status_string) +def on_message(simulation_id,message): + print("Message received:", simulation_id['message-id']) + + updates = dnp3_object_list[0].create_message_updates(simulation_id, message) + + print("Outstation Updates Created") + + for cimMapping in dnp3_object_list: + cimMapping.outstation.apply_compiled_updates(updates) + + print("Done updating outstations") + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('simulation_id', help="Simulation id") @@ -335,6 +347,7 @@ def publish_outstation_status(status_string): gapps = GridAPPSD(opts.simulation_id, address=utils.get_gridappsd_address(), username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + gapps.subscribe(simulation_output_topic(opts.simulation_id), on_message) dnp3_object_list = [] @@ -343,18 +356,14 @@ def publish_outstation_status(status_string): for obj in port_config: dnp3_object = DNP3Mapping(cim_dict) dnp3_object._create_dnp3_object_map() - gapps.subscribe(simulation_output_topic(opts.simulation_id),dnp3_object.on_message) if check_valid_points: - with open("/tmp/json_out", 'w') as fp: out_dict = dict({'points': dnp3_object.out_json}) json.dump(out_dict, fp, indent=2, sort_keys=True) - if not dnp3_object.out_json: sys.stderr.write("invalid points specified in json configuration file.") sys.exit(10) - check_valid_points = False oustation = obj @@ -365,6 +374,7 @@ def publish_outstation_status(status_string): outstation = start_outstation(oustation, processor) #for outstation in outstation_list: dnp3_object.load_outstation(outstation) + dnp3_object_list.append(dnp3_object) # gapps.send(simulation_input_topic(opts.simulation_id), processor.process_point_value()) try: From c8e3d40a2992c825b3704ca957d4debcf49dc970 Mon Sep 17 00:00:00 2001 From: Seth Gossler Date: Fri, 24 Jul 2020 13:49:45 -0700 Subject: [PATCH 16/64] Accepted the wrong Spelling in the merge. --- dnp3/service/dnp3/cim_to_dnp3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 0c12096..cee304f 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -373,7 +373,7 @@ def _create_dnp3_object_map(self): # bank_phase = list(m['bankPhases']) for n in range(0, 4): measurement_id = m.get("mRID") - name = m['bankname'] + '-' + m['bankPhases'] + name = m['bankName'] + '-' + m['bankPhases'] description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) self.c_ao += 1 From 263c0efdd3a01816965a0aa5c062b691f1054ae7 Mon Sep 17 00:00:00 2001 From: singha42 Date: Mon, 27 Jul 2020 04:54:08 -0700 Subject: [PATCH 17/64] updated service code --- dnp3/service/new_start_service.py | 32 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index 3a14ba7..ce2789b 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -104,24 +104,30 @@ def process_point_value(self, command_type, command, index, op_type): _log.debug("command_status={},command_value={}".format(command.status, command.value)) for point in self.outstation.get_agent().point_definitions.all_points(): # print(command.value, point.attribute) - if point.name in str(point_value.point_def) and "RegulatingControl.Mode" in point.attribute : + if point.name in str(point_value.point_def): self._diff.clear() self._diff.add_difference(point.measurement_id, point.attribute, command.value, 0) # value : received value msg = self._diff.get_message() self._gapps.send(self._publish_to_topic, json.dumps(msg)) print(json.dumps(msg)) - elif point.name in str(point_value.point_def) and "TapChanger.lineDropR" in point.attribute: - self._diff.clear() - self._diff.add_difference(point.measurement_id, point.attribute, command.value,0) - msg = self._diff.get_message() - self._gapps.send(self._publish_to_topic, json.dumps(msg)) - print(json.dumps(msg)) - elif point.name in str(point_value.point_def) and "Shunt" in point.attribute: - self._diff.clear() - self._diff.add_difference(point.measurement_id, point.attribute, command.value , 0) - msg = self._diff.get_message() - self._gapps.send(self._publish_to_topic, json.dumps(msg)) - print(json.dumps(msg)) + #if point.name in str(point_value.point_def) and "RegulatingControl.Mode" in point.attribute : + # self._diff.clear() + # self._diff.add_difference(point.measurement_id, point.attribute, command.value, 0) # value : received value + # msg = self._diff.get_message() + # self._gapps.send(self._publish_to_topic, json.dumps(msg)) + # print(json.dumps(msg)) + #elif point.name in str(point_value.point_def) and "TapChanger.lineDropR" in point.attribute: + # self._diff.clear() + # self._diff.add_difference(point.measurement_id, point.attribute, command.value,0) + # msg = self._diff.get_message() + # self._gapps.send(self._publish_to_topic, json.dumps(msg)) + # print(json.dumps(msg)) + #elif point.name in str(point_value.point_def) and "Shunt" in point.attribute: + # self._diff.clear() + # self._diff.add_difference(point.measurement_id, point.attribute, command.value , 0) + # msg = self._diff.get_message() + # self._gapps.send(self._publish_to_topic, json.dumps(msg)) + # print(json.dumps(msg)) if point_value is None: return opendnp3.CommandStatus.DOWNSTREAM_FAIL From 2eb9428afbb11bbd30fa62d0a14109db9d1bb9e3 Mon Sep 17 00:00:00 2001 From: singha42 Date: Tue, 1 Sep 2020 18:12:31 +0000 Subject: [PATCH 18/64] added AI points for regulators --- dnp3/service/dnp3/cim_to_dnp3.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index cee304f..cfcbb22 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -377,6 +377,8 @@ def _create_dnp3_object_map(self): description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) self.c_ao += 1 + self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id[0], reg_attribute[n]) + self.c_ai += 1 for i in range(5, 7): for j in range(0, len(m['bankPhases'])): measurement_id = m.get("mRID")[j] @@ -384,6 +386,8 @@ def _create_dnp3_object_map(self): description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) self.c_ao += 1 + self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id,reg_attribute[i]) + self.c_ai += 1 for m in solarpanels: for k in range(0, len(m['phases'])): From 29f6b4841a02302a17868b16ed205057e86b8051 Mon Sep 17 00:00:00 2001 From: singha42 <43796015+singha42@users.noreply.github.com> Date: Wed, 2 Sep 2020 11:06:50 -0700 Subject: [PATCH 19/64] Update new_start_service.py --- dnp3/service/new_start_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index ce2789b..75f712a 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -62,13 +62,13 @@ def process_point_value(self, command_type, command, index, op_type): if 'ON' in str(command.functionCode): self._diff.clear() self._diff.add_difference(point.measurement_id, point.attribute, 1, 0) - msg = self.diff.get_message() + msg = self._diff.get_message() self._gapps.send(self._publish_to_topic, json.dumps(msg)) print(json.dumps(msg)) else: self._diff.clear() self._diff.add_difference(point.measurement_id, point.attribute, 0, 1) - msg = self.diff.get_message() + msg = self._diff.get_message() self._gapps.send(self._publish_to_topic, json.dumps(msg)) print(json.dumps(msg)) if point.name in str(point_value.point_def) and point.attribute == 'Switch.open': From 50d731d2a3cfb39ee73cddadb625725bdf3bdf8a Mon Sep 17 00:00:00 2001 From: singha42 Date: Wed, 2 Sep 2020 20:37:46 +0000 Subject: [PATCH 20/64] iowa changes --- dnp3/service/dnp3/cim_to_dnp3.py | 2 +- dnp3/service/new_start_service.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index cfcbb22..b2bae7d 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -294,7 +294,7 @@ def _create_dnp3_object_map(self): if grpM[0]['MeasurementClass'] == "Analog" and grpM[0].get("measurementType") == "VA": measurement_type = grpM[0].get("measurementType") - measurement_id = grpM[0].get("mRID") +","+ grpM[1].get("mRID") +","+ grpM[2].get("mRID") + measurement_id = m.get("mRID") name1 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VAR-value' diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index ce2789b..2ec58b2 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -58,17 +58,17 @@ def process_point_value(self, command_type, command, index, op_type): if 'Control' in str(command): _log.debug("command_code={},command_code={},command_ontime={}".format(command.status, command.functionCode, command.onTimeMS)) for point in self.outstation.get_agent().point_definitions.all_points(): - if point.name in str(point_value.point_def) and point.attribute == 'ShuntCompensator.sections': + if point.name in str(point_value.point_def) and point.attribute == 'ShuntCompensator.sections': if 'ON' in str(command.functionCode): self._diff.clear() self._diff.add_difference(point.measurement_id, point.attribute, 1, 0) - msg = self.diff.get_message() + msg = self._diff.get_message() self._gapps.send(self._publish_to_topic, json.dumps(msg)) print(json.dumps(msg)) else: self._diff.clear() self._diff.add_difference(point.measurement_id, point.attribute, 0, 1) - msg = self.diff.get_message() + msg = self._diff.get_message() self._gapps.send(self._publish_to_topic, json.dumps(msg)) print(json.dumps(msg)) if point.name in str(point_value.point_def) and point.attribute == 'Switch.open': @@ -324,16 +324,16 @@ def publish_outstation_status(status_string): print(status_string) def on_message(simulation_id,message): - print("Message received:", simulation_id['message-id']) + #print("Message received:", simulation_id['message-id']) updates = dnp3_object_list[0].create_message_updates(simulation_id, message) - print("Outstation Updates Created") + # print("Outstation Updates Created") for cimMapping in dnp3_object_list: cimMapping.outstation.apply_compiled_updates(updates) - print("Done updating outstations") + #print("Done updating outstations") if __name__ == '__main__': parser = argparse.ArgumentParser() @@ -341,7 +341,7 @@ def on_message(simulation_id,message): opts = parser.parse_args() simulation_id = opts.simulation_id - with open("/tmp/port.json", 'r') as f: + with open("/gridappsd/services/gridappsd-dnp3/service/dnp3/port.json", 'r') as f: port_config = json.load(f) print(port_config) From aae32c42d3277972c077721d66d741db198dca34 Mon Sep 17 00:00:00 2001 From: Seth Gossler Date: Wed, 2 Sep 2020 14:31:48 -0700 Subject: [PATCH 21/64] I've made some updates around the final items. I'm putting them into this new branch while checking out Alkas changes. I should be merging this branch back or her changes here. --- dnp3/service/dnp3/cim_to_dnp3.py | 54 ++++++++++++++++--------------- dnp3/service/new_start_service.py | 16 +++++---- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index cee304f..a0697ae 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -5,7 +5,7 @@ import random import uuid import math -# import pydevd;pydevd.settrace(suspend=False) # Uncomment For Debugging on other Threads +#import pydevd;pydevd.settrace(suspend=False) # Uncomment For Debugging on other Threads from collections import defaultdict from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 @@ -21,7 +21,7 @@ attribute_map = { "capacitors": { - "attribute": ["RegulatingControl.mode", "RegulatingControl.targetDeadband", "RegulatingControl.targetValue", + "attribute": ["RegulatingControl.enabled", "RegulatingControl.mode", "RegulatingControl.targetDeadband", "RegulatingControl.targetValue", "ShuntCompensator.aVRDelay", "ShuntCompensator.sections"]} , "switches": { @@ -272,17 +272,19 @@ def _create_dnp3_object_map(self): breakers = list() reclosers = list() energyconsumers = list() + + # Added sorting on MRID to maintain Index Orders for dnp3 index generation between different containers for x in feeders: - measurements = x.get("measurements", []) - capacitors = x.get("capacitors", []) - regulators = x.get("regulators", []) - switches = x.get("switches", []) - solarpanels = x.get("solarpanels", []) - batteries = x.get("batteries", []) - fuses = x.get("fuses", []) - breakers = x.get("breakers", []) - reclosers = x.get("reclosers", []) - energyconsumers = x.get("energyconsumers", []) + measurements = sorted(x.get("measurements", []), key=lambda x: x['mRID'], reverse=True) + capacitors = sorted(x.get("capacitors", []), key=lambda x: x['mRID'], reverse=True) + regulators = sorted(x.get("regulators", []), key=lambda x: x['mRID'], reverse=True) + switches = sorted(x.get("switches", []), key=lambda x: x['mRID'], reverse=True) + solarpanels = sorted(x.get("solarpanels", []), key=lambda x: x['mRID'], reverse=True) + batteries = sorted(x.get("batteries", []), key=lambda x: x['mRID'], reverse=True) + fuses = sorted(x.get("fuses", []), key=lambda x: x['mRID'], reverse=True) + breakers = sorted(x.get("breakers", []), key=lambda x: x['mRID'], reverse=True) + reclosers = sorted(x.get("reclosers", []), key=lambda x: x['mRID'], reverse=True) + energyconsumers = sorted(x.get("energyconsumers", []), key=lambda x: x['mRID'], reverse=True) # Unique grouping of measurements - GroupBy Name, Type and Connectivity node groupByNameTypeConNode = defaultdict(list) @@ -344,7 +346,7 @@ def _create_dnp3_object_map(self): elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": if "RatioTapChanger" in m['name'] or "reg" in m["SimObject"]: # TODO: Do we need step? - for r in range(5, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX + for r in range(4, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) self.c_ao += 1 else: @@ -355,18 +357,18 @@ def _create_dnp3_object_map(self): measurement_id = m.get("mRID") cap_attribute = attribute_map['capacitors']['attribute'] # type: List[str] - for l in range(0, 4): + for l in range(0, 6): # publishing attribute value for capacitors as Bianry/Analog Input points based on phase attribute name = m['name'] - description = "Name:" + m['name'] + "ConductingEquipment_type:LinearShuntCompensator" + ",Attribute:" + cap_attribute[l] + ",Phase:" + m['phases'] + description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",Attribute:" + cap_attribute[l] + ",Phase:" + m['phases'] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, cap_attribute[l]) self.c_ao += 1 - for p in range(0, len(m['phases'])): - name = m['name'] + m['phases'][p] - description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[p] + ",Phase:" + m['phases'][p] - # description = "Capacitor, " + m['name'] + "," + "phase -" + m['phases'][p] + ", and attribute is - " + cap_attribute[4] - self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[4]) - self.c_do += 1 + for p in range(0, len(m['phases'])): + name = m['name'] + m['phases'][p] + description = "Name:" + m['name'] + ",ConductingEquipment_type:LinearShuntCompensator" + ",controlAttribute:" + cap_attribute[l] + ",Phase:" + m['phases'][p] + # description = "Capacitor, " + m['name'] + "," + "phase -" + m['phases'][p] + ", and attribute is - " + cap_attribute[l] + self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, cap_attribute[l]) + self.c_do += 1 for m in regulators: reg_attribute = attribute_map['regulators']['attribute'] @@ -389,7 +391,7 @@ def _create_dnp3_object_map(self): for k in range(0, len(m['phases'])): measurement_id = m.get("mRID") name = "Solar" + m['name'] + '-' + m['phases'][k] + '-Watts-value' - description = "Solarpanel:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id + description = "Type:Solarpanel, Name:" + m['name'] + ",Phase:" + m['phases'] + ",measurementID:" + measurement_id self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, "PowerElectronicsConnection.p") self.c_ao += 1 @@ -409,7 +411,7 @@ def _create_dnp3_object_map(self): for l in range(0, len(m['phases'])): measurement_id = m.get("mRID") name = m['name'] + '-' + m['phases'][l] + '-Watts-value' - description = "Battery, " + m['name'][l] + ",Phase: " + m['phases'] + ",ConductingEquipment_type:PowerElectronicConnections" + description = "Type:Battery,Name:" + m['name']+ ",Phase:" + m['phases'][l] + ",ConductingEquipment_type:PowerElectronicConnections" self.assign_val_d("AO", 42, 3, self.c_ao, name, description,measurement_id, "PowerElectronicsConnection.p") self.c_ao += 1 name1 = m['name'] + '-' + m['phases'][l] + '-VAR-value' @@ -442,7 +444,7 @@ def _create_dnp3_object_map(self): for n in range(0, len(m['phases'])): phase_value = list(m['phases']) name = m['name'] + "Phase:" + m['phases'][n] - description = "Name: " + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute + description = "Name:" + m["name"] + ",Phase:" + phase_value[n] + ",ConductingEquipment_type:Breaker" + ",controlAttribute:" + switch_attribute self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 @@ -452,7 +454,7 @@ def _create_dnp3_object_map(self): for i in range(0, len(m['phases'])): phase_value = list(m['phases']) name = m['name'] + "Phase:" + m['phases'][i] - description = "Recloser, " + m["name"] + "Phase: - " + phase_value[i] + ",ConductingEquipment_type:Recloser"+"controlAttribute:" + switch_attribute + description = "Name:" + m["name"] + ",Phase:" + phase_value[i] + ",ConductingEquipment_type:Recloser,"+"controlAttribute:" + switch_attribute self.assign_val_d("DO", 12, 1, self.c_do, name, description, measurement_id, switch_attribute) self.c_do += 1 @@ -461,7 +463,7 @@ def _create_dnp3_object_map(self): for k in range(0, len(m['phases'])): phase_value = list(m['phases']) name = m['name']+"phase:" + m['phases'][k] - description = "EnergyConsumer, " + m["name"] + "Phase: " + phase_value[k] + description = "Name:" + m['name'] + ",ConductingEquipment_type:EnergyConsumer,Phase:" + phase_value[k] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, "EnergyConsumer.p") self.c_ao += 1 diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index ce2789b..1583c4f 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -3,6 +3,8 @@ import numbers import sys import json +#import pydevd;pydevd.settrace(suspend=False) # Uncomment For Debugging on other Threads + from time import sleep from yaml import safe_load @@ -325,15 +327,15 @@ def publish_outstation_status(status_string): def on_message(simulation_id,message): print("Message received:", simulation_id['message-id']) + if(dnp3_object_list.__len__() > 0): + updates = dnp3_object_list[0].create_message_updates(simulation_id, message) - updates = dnp3_object_list[0].create_message_updates(simulation_id, message) - - print("Outstation Updates Created") + print("Outstation Updates Created") - for cimMapping in dnp3_object_list: - cimMapping.outstation.apply_compiled_updates(updates) - - print("Done updating outstations") + for cimMapping in dnp3_object_list: + cimMapping.outstation.apply_compiled_updates(updates) + + print("Done updating outstations") if __name__ == '__main__': parser = argparse.ArgumentParser() From 509fbc4ae3661a5984b1467eca0fdbdef079fb43 Mon Sep 17 00:00:00 2001 From: singha42 <43796015+singha42@users.noreply.github.com> Date: Wed, 9 Sep 2020 11:32:34 -0700 Subject: [PATCH 22/64] Update cim_to_dnp3.py --- dnp3/service/dnp3/cim_to_dnp3.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index b2bae7d..7d70ee0 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -344,9 +344,9 @@ def _create_dnp3_object_map(self): elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": if "RatioTapChanger" in m['name'] or "reg" in m["SimObject"]: # TODO: Do we need step? - for r in range(5, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX - self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) - self.c_ao += 1 + for r in range(0, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX + self.assign_val_d("AI", 30, 1, self.c_ao, name, description, measurement_id, attribute[r]) + self.c_ai += 1 else: self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) self.c_di += 1 @@ -377,17 +377,17 @@ def _create_dnp3_object_map(self): description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) self.c_ao += 1 - self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id[0], reg_attribute[n]) - self.c_ai += 1 - for i in range(5, 7): + #self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id[0], reg_attribute[n]) + #self.c_ai += 1 + for i in range(4, 7): for j in range(0, len(m['bankPhases'])): measurement_id = m.get("mRID")[j] name = m['tankName'][j] + '-' + m['bankPhases'][j] description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) self.c_ao += 1 - self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id,reg_attribute[i]) - self.c_ai += 1 + #self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id,reg_attribute[i]) + #self.c_ai += 1 for m in solarpanels: for k in range(0, len(m['phases'])): From 027c8b18bd65c22da86a8245721fbdda7e18712f Mon Sep 17 00:00:00 2001 From: singha42 Date: Thu, 17 Sep 2020 23:03:50 +0000 Subject: [PATCH 23/64] correct analog values --- dnp3/service/dnp3/cim_to_dnp3.py | 52 ++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 7d70ee0..33d5d29 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -136,6 +136,7 @@ def create_message_updates(self, simulation_id, message): # fncs_input_message = {"{}".format(simulation_id): {}} measurement_values = json_msg["message"]["measurements"] + #print(measurement_values) # Calculate each net-phase measurement if(measurement_values): @@ -145,6 +146,7 @@ def create_message_updates(self, simulation_id, message): for point in netPoints: ptMeasurements = list(filter(lambda m: m.get("measurement_mrid") in point.measurement_id, measurement_values.values())) + #print(type(measurement_values.values())) netValue = 0.0 for m in ptMeasurements: @@ -161,7 +163,7 @@ def create_message_updates(self, simulation_id, message): point.magnitude = netValue builder.Update(opendnp3.Analog(point.magnitude), point.index) - + #print("==========", point.magnitude, point.index) # Calculate each measurement for y in measurement_values: # print(self.processor_point_def.points_by_mrid()) @@ -196,6 +198,7 @@ def create_message_updates(self, simulation_id, message): if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): point.value = m.get("value") builder.Update(opendnp3.Binary(point.value), point.index) + print("==========", point.value, point.index, point.measurement_id) # Return the atomic "updates" object print("Updates Created") @@ -294,23 +297,26 @@ def _create_dnp3_object_map(self): if grpM[0]['MeasurementClass'] == "Analog" and grpM[0].get("measurementType") == "VA": measurement_type = grpM[0].get("measurementType") - measurement_id = m.get("mRID") - + measurement_id = "" + for m in grpM: + + measurement_id += m.get("mRID") + "," + #measurement_id = grpM[0].get("mRID") + "," + grpM[1].get("mRID")+ "," + grpM[2].get("mRID") - name1 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VAR-value' - name2 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-Watts-value' - name3 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VA-value' + name1 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VAR-value' + name2 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-Watts-value' + name3 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VA-value' - description1 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VAR" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") - description2 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-Watts" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") - description3 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VA" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description1 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VAR" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description2 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-Watts" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description3 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VA" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") - self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) - self.c_ai += 1 - self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) - self.c_ai += 1 - self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) - self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) + self.c_ai += 1 # Create Each Phase DNP3 Points for m in measurements: @@ -344,9 +350,9 @@ def _create_dnp3_object_map(self): elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": if "RatioTapChanger" in m['name'] or "reg" in m["SimObject"]: # TODO: Do we need step? - for r in range(0, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX - self.assign_val_d("AI", 30, 1, self.c_ao, name, description, measurement_id, attribute[r]) - self.c_ai += 1 + for r in range(5, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX + self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) + self.c_ao += 1 else: self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) self.c_di += 1 @@ -377,17 +383,17 @@ def _create_dnp3_object_map(self): description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) self.c_ao += 1 - #self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id[0], reg_attribute[n]) - #self.c_ai += 1 - for i in range(4, 7): + self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id[0], reg_attribute[n]) + self.c_ai += 1 + for i in range(5, 7): for j in range(0, len(m['bankPhases'])): measurement_id = m.get("mRID")[j] name = m['tankName'][j] + '-' + m['bankPhases'][j] description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) self.c_ao += 1 - #self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id,reg_attribute[i]) - #self.c_ai += 1 + self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id,reg_attribute[i]) + self.c_ai += 1 for m in solarpanels: for k in range(0, len(m['phases'])): From cf5c18707a73cd61a037f5c26a3bfa681d9093a1 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Mon, 21 Sep 2020 15:32:53 -0600 Subject: [PATCH 24/64] Updated to use master.py --- dnp3/dnp3.config | 2 +- dnp3/service/dnp3/cim_to_dnp3.py | 51 ++++- dnp3/service/dnp3/master.py | 345 ++++++++++++++++++++++++++++++ dnp3/service/dnp3/outstation.py | 8 +- dnp3/service/new_start_service.py | 39 +++- 5 files changed, 424 insertions(+), 21 deletions(-) create mode 100644 dnp3/service/dnp3/master.py diff --git a/dnp3/dnp3.config b/dnp3/dnp3.config index 83ccfc7..8d52a5f 100644 --- a/dnp3/dnp3.config +++ b/dnp3/dnp3.config @@ -5,7 +5,7 @@ "input_topics": [], "output_topics": [], "static_args": ["(simulationId)"], - "execution_path": "/gridappsd/services/gridappsd-dnp3/service/new_start_service.py", + "execution_path": "/gridappsd/services/dnp3/service/new_start_service.py", "type": "PYTHON", "launch_on_startup": "true", "service_dependencies": [], diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index 739da32..ed36891 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -12,9 +12,11 @@ PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT ) +from dnp3.master import command_callback, restart_callback + out_json = list() -'''Dictionary for mapping the attribute values of control poitns for Capacitor, Regulator and Switches''' +'''Dictionary for mapping the attribute values of control pointd for Capacitor, Regulator and Switches, Mesurements''' attribute_map = { "capacitors": { @@ -29,11 +31,14 @@ "regulators": { "attribute": ["RegulatingControl.targetDeadband", "RegulatingControl.targetValue", "TapChanger.initialDelay", "TapChanger.lineDropCompensation", "TapChanger.step", "TapChanger.lineDropR", - "TapChanger.lineDropX"]} - -} + "TapChanger.lineDropX"]}, + "measurements": { + "attribute": ["measurement_mrid", "type", "magnitude", "angle", "value"]} + # ["Discrete", "Analog" ,"Measurement.PowerSystemResource", "Measurement.Terminal", + # "Measurement.phases","Measurement.measurementType"] ## TODO check against points file +} class DNP3Mapping(): """ This creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" @@ -47,9 +52,10 @@ def __init__(self, map_file): self.file_dict = map_file self.processor_point_def = PointDefinitions() self.outstation = DNP3Outstation('',0,'') + self.master = None - def on_message(self, simulation_id,message): + def on_message(self, simulation_id, message): """ This method handles incoming messages on the fncs_output_topic for the simulation_id. Parameters ---------- @@ -62,8 +68,15 @@ def on_message(self, simulation_id,message): try: message_str = 'received message ' + str(message) - + print(message_str) json_msg = yaml.safe_load(str(message)) + # self.master.apply_update(opendnp3.Binary(0), 12) + + self.master.send_direct_operate_command( + opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 5, + command_callback) + if type(json_msg) != dict: raise ValueError( @@ -71,7 +84,28 @@ def on_message(self, simulation_id,message): + '\njson_msg = {0}'.format(json_msg)) # fncs_input_message = {"{}".format(simulation_id): {}} - measurement_values = json_msg["message"]["measurements"] + # measurement_values = json_msg["message"]["measurements"] + measurement_values = [] + + print(json_msg) + # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} + control_values = json_msg["input"]["message"]["forward_differences"] + print(control_values) + # exit(0) + + ## TODO get command + for command in control_values: + print("command", command) + print(self.master) + for point in self.master.get_agent().point_definitions.all_points(): + # print("point",point) + # print("y",y) + if command.get("object") == point.measurement_id and point.value != command.get("value"): + print("point", point) + point.magnitude = command.get("value") + print(opendnp3.Binary(point.value), point.index) + self.master.apply_update(opendnp3.Binary(point.value), point.index) + # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values for y in measurement_values: @@ -146,6 +180,9 @@ def load_point_def(self, point_def): def load_outstation(self, outstation): self.outstation = outstation + def load_master(self, master): + self.master = master + def _create_dnp3_object_map(self): """This method creates the points by taking the input data from model dictionary file""" diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py new file mode 100644 index 0000000..03f2095 --- /dev/null +++ b/dnp3/service/dnp3/master.py @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2018, SLAC / 8minutenergy / Kisensum. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared in part as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor SLAC, nor 8minutenergy, nor Kisensum, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# SLAC, 8minutenergy, or Kisensum. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# }}} + +import logging +import sys +import time + +from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 +from dnp3.visitors import * + +FILTERS = opendnp3.levels.NORMAL | opendnp3.levels.ALL_COMMS +FILTERS = opendnp3.levels.NOTHING +HOST = "127.0.0.1" +HOST = "192.168.1.2" +LOCAL = "0.0.0.0" +# LOCAL = "192.168.1.2" +PORT = 20000 + + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) + + +class MyMaster: + """ + Interface for all master application callback info except for measurement values. + + DNP3 spec section 5.1.6.1: + The Application Layer provides the following services for the DNP3 User Layer in a master: + - Formats requests directed to one or more outstations. + - Notifies the DNP3 User Layer when new data or information arrives from an outstation. + + DNP spec section 5.1.6.3: + The Application Layer requires specific services from the layers beneath it. + - Partitioning of fragments into smaller portions for transport reliability. + - Knowledge of which device(s) were the source of received messages. + - Transmission of messages to specific devices or to all devices. + - Message integrity (i.e., error-free reception and transmission of messages). + - Knowledge of the time when messages arrive. + - Either precise times of transmission or the ability to set time values + into outgoing messages. + """ + def __init__(self, + log_handler=asiodnp3.ConsoleLogger().Create(), + listener=asiodnp3.PrintingChannelListener().Create(), + soe_handler=asiodnp3.PrintingSOEHandler().Create(), + master_application=asiodnp3.DefaultMasterApplication().Create(), + stack_config=None): + + _log.debug('Creating a DNP3Manager.') + self.log_handler = log_handler + self.manager = asiodnp3.DNP3Manager(1, self.log_handler) + + _log.debug('Creating the DNP3 channel, a TCP client.') + self.retry = asiopal.ChannelRetry().Default() + self.listener = listener + self.channel = self.manager.AddTCPClient("tcpclient", + FILTERS, + self.retry, + HOST, + LOCAL, + PORT, + self.listener) + + _log.debug('Configuring the DNP3 stack.') + self.stack_config = stack_config + if not self.stack_config: + self.stack_config = asiodnp3.MasterStackConfig() + self.stack_config.master.responseTimeout = openpal.TimeDuration().Seconds(2) + self.stack_config.link.RemoteAddr = 1024 ## TODO get from config Was 10 + + _log.debug('Adding the master to the channel.') + self.soe_handler = soe_handler + self.master_application = master_application + self.master = self.channel.AddMaster("master", + asiodnp3.PrintingSOEHandler().Create(), + self.master_application, + self.stack_config) + + _log.debug('Configuring some scans (periodic reads).') + # Set up a "slow scan", an infrequent integrity poll that requests events and static data for all classes. + self.slow_scan = self.master.AddClassScan(opendnp3.ClassField().AllClasses(), + openpal.TimeDuration().Minutes(30), + opendnp3.TaskConfig().Default()) + # Set up a "fast scan", a relatively-frequent exception poll that requests events and class 1 static data. + self.fast_scan = self.master.AddClassScan(opendnp3.ClassField(opendnp3.ClassField.CLASS_1), + openpal.TimeDuration().Minutes(1), + opendnp3.TaskConfig().Default()) + + # self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + # self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.NOTHING)) + self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.NOTHING)) + + + _log.debug('Enabling the master. At this point, traffic will start to flow between the Master and Outstations.') + self.master.Enable() + time.sleep(5) + + @classmethod + def get_agent(cls): + """Return the singleton DNP3Agent """ + agt = cls.agent + if agt is None: + raise ValueError('Master has no configured agent') + return agt + + @classmethod + def set_agent(cls, agent): + """Set the singleton DNP3Agent """ + cls.agent = agent + + def send_direct_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Direct operate a single command + + :param command: command to operate + :param index: index of the command + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.DirectOperate(command, index, callback, config) + + def send_direct_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Direct operate a set of commands + + :param command_set: set of command headers + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.DirectOperate(command_set, callback, config) + + def send_select_and_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Select and operate a single command + + :param command: command to operate + :param index: index of the command + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.SelectAndOperate(command, index, callback, config) + + def send_select_and_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Select and operate a set of commands + + :param command_set: set of command headers + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.SelectAndOperate(command_set, callback, config) + + def shutdown(self): + del self.slow_scan + del self.fast_scan + del self.master + del self.channel + self.manager.Shutdown() + + +class MyLogger(openpal.ILogHandler): + """ + Override ILogHandler in this manner to implement application-specific logging behavior. + """ + + def __init__(self): + super(MyLogger, self).__init__() + + def Log(self, entry): + flag = opendnp3.LogFlagToString(entry.filters.GetBitfield()) + filters = entry.filters.GetBitfield() + location = entry.location.rsplit('/')[-1] if entry.location else '' + message = entry.message + _log.debug('LOG\t\t{:<10}\tfilters={:<5}\tlocation={:<25}\tentry={}'.format(flag, filters, location, message)) + # pass + + +class AppChannelListener(asiodnp3.IChannelListener): + """ + Override IChannelListener in this manner to implement application-specific channel behavior. + """ + + def __init__(self): + super(AppChannelListener, self).__init__() + + def OnStateChange(self, state): + # _log.debug('In AppChannelListener.OnStateChange: state={}'.format(opendnp3.ChannelStateToString(state))) + pass + + +class SOEHandler(opendnp3.ISOEHandler): + """ + Override ISOEHandler in this manner to implement application-specific sequence-of-events behavior. + + This is an interface for SequenceOfEvents (SOE) callbacks from the Master stack to the application layer. + """ + + def __init__(self): + super(SOEHandler, self).__init__() + + def Process(self, info, values): + """ + Process measurement data. + + :param info: HeaderInfo + :param values: A collection of values received from the Outstation (various data types are possible). + """ + visitor_class_types = { + opendnp3.ICollectionIndexedBinary: VisitorIndexedBinary, + opendnp3.ICollectionIndexedDoubleBitBinary: VisitorIndexedDoubleBitBinary, + opendnp3.ICollectionIndexedCounter: VisitorIndexedCounter, + opendnp3.ICollectionIndexedFrozenCounter: VisitorIndexedFrozenCounter, + opendnp3.ICollectionIndexedAnalog: VisitorIndexedAnalog, + opendnp3.ICollectionIndexedBinaryOutputStatus: VisitorIndexedBinaryOutputStatus, + opendnp3.ICollectionIndexedAnalogOutputStatus: VisitorIndexedAnalogOutputStatus, + opendnp3.ICollectionIndexedTimeAndInterval: VisitorIndexedTimeAndInterval + } + visitor_class = visitor_class_types[type(values)] + visitor = visitor_class() + values.Foreach(visitor) + for index, value in visitor.index_and_value: + log_string = 'SOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + + def Start(self): + _log.debug('In SOEHandler.Start') + + def End(self): + _log.debug('In SOEHandler.End') + + +class MasterApplication(opendnp3.IMasterApplication): + def __init__(self): + super(MasterApplication, self).__init__() + + # Overridden method + def AssignClassDuringStartup(self): + _log.debug('In MasterApplication.AssignClassDuringStartup') + return False + + # Overridden method + def OnClose(self): + _log.debug('In MasterApplication.OnClose') + + # Overridden method + def OnOpen(self): + _log.debug('In MasterApplication.OnOpen') + + # Overridden method + def OnReceiveIIN(self, iin): + _log.debug('In MasterApplication.OnReceiveIIN') + + # Overridden method + def OnTaskComplete(self, info): + _log.debug('In MasterApplication.OnTaskComplete') + + # Overridden method + def OnTaskStart(self, type, id): + _log.debug('In MasterApplication.OnTaskStart') + + +def collection_callback(result=None): + """ + :type result: opendnp3.CommandPointResult + """ + print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( + result.headerIndex, + result.index, + opendnp3.CommandPointStateToString(result.state), + opendnp3.CommandStatusToString(result.status) + )) + + +def command_callback(result=None): + """ + :type result: opendnp3.ICommandTaskResult + """ + print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) + result.ForeachItem(collection_callback) + + +def restart_callback(result=opendnp3.RestartOperationResult()): + if result.summary == opendnp3.TaskCompletion.SUCCESS: + print("Restart success | Restart Time: {}".format(result.restartTime.GetMilliseconds())) + else: + print("Restart fail | Failure: {}".format(opendnp3.TaskCompletionToString(result.summary))) + + +def main(): + """The Master has been started from the command line. Execute ad-hoc tests if desired.""" + # app = MyMaster() + app = MyMaster(log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(), + master_application=MasterApplication()) + _log.debug('Initialization complete. In command loop.') + # Ad-hoc tests can be performed at this point. See master_cmd.py for examples. + app.shutdown() + _log.debug('Exiting.') + exit() + + +if __name__ == '__main__': + main() diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index 5c25e57..383e6ff 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -93,6 +93,7 @@ def __init__(self, local_ip, port, outstation_config): super(DNP3Outstation, self).__init__() self.local_ip = local_ip self.port = port + print("JEFF port", port) #print(local_ip), self.set_outstation_config(outstation_config) # The following variables are initialized after start() is called. @@ -236,6 +237,7 @@ def dnp3_log_level(self): use a union of those names to construct the integer. Otherwise return the default log level. """ log_level_list = self.outstation_config.get('log_levels', ['NORMAL']) + log_level_list = [] # log_level_list should be a list of strings. If it's not (e.g., if it's a simple string), fail. if not isinstance(log_level_list, list): raise TypeError('log_levels should be configured as a list of strings, not as {}'.format(log_level_list)) @@ -248,11 +250,11 @@ def dnp3_log_level(self): 'NORMAL': opendnp3.levels.NORMAL, 'NOTHING': opendnp3.levels.NOTHING } - log_level = 0 + log_level = 6 for name in log_level_list: log_level = log_level | name_to_bitmasks.get(name, 0) - _log.debug('Setting DNP3 log level={} ({})'.format(log_level, log_level_list)) + _log.debug('Setting DNP3 JEFF log level={} ({})'.format(log_level, log_level_list)) return log_level # Overridden method @@ -309,7 +311,7 @@ def apply_update(cls, value, index): :param value: An instance of Analog, Binary, or another opendnp3 data value. :param index: (integer) Index of the data definition in the opendnp3 database. """ - #_log.debug('Recording DNP3 {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value)) + _log.debug('Recording DNP3 {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value)) max_index = cls.get_outstation_config().get('database_sizes', 10000) if index > max_index: raise ValueError('Attempt to set a value for index {} which exceeds database size {}'.format(index, diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index 81babbf..a9c9e1d 100644 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -16,6 +16,10 @@ ) from dnp3.outstation import DNP3Outstation +from pydnp3 import opendnp3, openpal +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.master import command_callback, restart_callback + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format='%(asctime)s:%(name)s:%(levelname)s: %(message)s') @@ -34,6 +38,7 @@ def __init__(self, point_definitions,simulation_id, gridappsd_obj): # self._close_diff = DifferenceBuilder(simulation_id) self._publish_to_topic = simulation_input_topic(simulation_id) self.processor_point_def = PointDefinitions() + print("Jeff") self.outstation = DNP3Outstation('', 0, '') def publish_outstation_status(self, status): @@ -322,11 +327,12 @@ def publish_outstation_status(status_string): if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('simulation_id', help="Simulation id") + parser.add_argument('simulation_id', help="Simulation id",nargs='*', default=1) opts = parser.parse_args() - simulation_id = opts.simulation_id + simulation_id = opts.simulation_id[0] filepath = "/tmp/gridappsd_tmp/{}/model_dict.json".format(simulation_id) + filepath = "../model_dict.json".format(simulation_id) with open(filepath, 'r') as fp: cim_dict = json.load(fp) dnp3_object = DNP3Mapping(cim_dict) @@ -336,7 +342,8 @@ def publish_outstation_status(status_string): out_dict = dict({'points': dnp3_object.out_json}) json.dump(out_dict, fp, indent=2, sort_keys=True) - with open("/tmp/port.json", 'r') as f: + # with open("/tmp/port.json", 'r') as f: + with open("./dnp3/port.json", 'r') as f: port_config = json.load(f) print(port_config) @@ -346,7 +353,11 @@ def publish_outstation_status(status_string): gapps = GridAPPSD(opts.simulation_id, address=utils.get_gridappsd_address(), username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) - gapps.subscribe(simulation_output_topic(opts.simulation_id),dnp3_object.on_message) + + print("subscribe " + simulation_input_topic(simulation_id)) + # gapps.subscribe(simulation_input_topic(simulation_id), dnp3_object.on_message) + # gapps.subscribe('/topic/goss.gridappsd.simulation.input.'+str(simulation_id), dnp3_object.on_message) + gapps.subscribe('/topic/goss.gridappsd.simulation.output.'+str(simulation_id), dnp3_object.on_message) oustation = dict() @@ -354,13 +365,21 @@ def publish_outstation_status(status_string): point_def.load_points(dnp3_object.out_json) processor = Processor(point_def, simulation_id, gapps) dnp3_object.load_point_def(point_def) - outstation_list = start_outstation(oustation, processor) - for outstation in outstation_list: - dnp3_object.load_outstation(outstation) - # gapps.send(simulation_input_topic(opts.simulation_id), processor.process_point_value()) - + # outstation_list = start_outstation(oustation, processor) + # for outstation in outstation_list: + # dnp3_object.load_outstation(outstation) + # # gapps.send(simulation_input_topic(simulation_id), processor.process_point_value()) + app = MyMaster(log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(), + master_application=MasterApplication()) + app.set_agent(processor) + dnp3_object.load_master(app) + # gapps.send(simulation_output_topic(simulation_id), processor.process_point_value()) try: while True: sleep(0.01) finally: - outstation.shutdown() + # outstation.shutdown() + app.shutdown() + pass From c321619a4315fd3d49eefd0bc8c7506ce7672bb9 Mon Sep 17 00:00:00 2001 From: singha42 <43796015+singha42@users.noreply.github.com> Date: Thu, 24 Sep 2020 14:47:45 -0700 Subject: [PATCH 25/64] Update new_start_service.py --- dnp3/service/new_start_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index 2ec58b2..cfea1ed 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -104,7 +104,7 @@ def process_point_value(self, command_type, command, index, op_type): _log.debug("command_status={},command_value={}".format(command.status, command.value)) for point in self.outstation.get_agent().point_definitions.all_points(): # print(command.value, point.attribute) - if point.name in str(point_value.point_def): + if point.name in str(point_value.point_def) and point.index==index: self._diff.clear() self._diff.add_difference(point.measurement_id, point.attribute, command.value, 0) # value : received value msg = self._diff.get_message() From eb52a58237dc19c2ae0248e97656d4eb392858d4 Mon Sep 17 00:00:00 2001 From: Seth Gossler Date: Thu, 24 Sep 2020 16:13:06 -0700 Subject: [PATCH 26/64] Updated to fix a few bugs and to include my debugging file. --- .gitignore | 2 + dnp3/service/dnp3/cim_to_dnp3.py | 148 ++++++++++-------------------- dnp3/service/dnp3/oestester.py | 53 +++++++++++ dnp3/service/new_start_service.py | 5 +- 4 files changed, 103 insertions(+), 105 deletions(-) create mode 100644 dnp3/service/dnp3/oestester.py diff --git a/.gitignore b/.gitignore index 1ab4360..e9d8ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ docs/build env/ *.swp +__pycache__/ +*.py[cod] diff --git a/dnp3/service/dnp3/cim_to_dnp3.py b/dnp3/service/dnp3/cim_to_dnp3.py index e9cfaa2..b82ad83 100755 --- a/dnp3/service/dnp3/cim_to_dnp3.py +++ b/dnp3/service/dnp3/cim_to_dnp3.py @@ -1,3 +1,6 @@ +import sys +sys.path.append(".") + import json import yaml import sys @@ -6,6 +9,7 @@ import uuid import math #import pydevd;pydevd.settrace(suspend=False) # Uncomment For Debugging on other Threads +from .oestester import OesTester from collections import defaultdict from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 @@ -52,67 +56,6 @@ def __init__(self, map_file): self.processor_point_def = PointDefinitions() self.outstation = DNP3Outstation('',0,'') - # This is the old on_message. Should probably be removed, unless the "start_service.py" needs it. Leaving this up to PNNL to decide :) - def on_message(self, simulation_id,message): - """ This method handles incoming messages on the fncs_output_topic for the simulation_id. - Parameters - ---------- - headers: dict - A dictionary of headers that could be used to determine topic of origin and - other attributes. - message: object - - """ - - try: - message_str = 'received message ' + str(message) - - json_msg = yaml.safe_load(str(message)) - - if type(json_msg) != dict: - raise ValueError( - ' is not a json formatted string.' - + '\njson_msg = {0}'.format(json_msg)) - - # fncs_input_message = {"{}".format(simulation_id): {}} - measurement_values = json_msg["message"]["measurements"] - - # storing the magnitude and measurement_mRID values to publish in the dnp3 points for measurement key values - for y in measurement_values: - # print(self.processor_point_def.points_by_mrid()) - m = measurement_values[y] - if "magnitude" in m.keys(): - for point in self.outstation.get_agent().point_definitions.all_points(): - #print("point",point) - #print("y",y) - if m.get("measurement_mrid") == point.measurement_id and point.magnitude != m.get("magnitude"): - point.magnitude = m.get("magnitude") - self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - - elif point.measurement_type == "VA" and "VAR" in point.name: - angle = math.radians(m.get("angle")) - point.magnitude = math.sin(angle) * m.get("magnitude") - self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - - elif point.measurement_type == "VA" and "Watts" in point.name: - angle1 = math.radians(m.get("angle")) - point.magnitude = math.cos(angle1) * m.get("magnitude") - self.outstation.apply_update(opendnp3.Analog(point.magnitude), point.index) - - elif point.measurement_type == "VA" and "angle" in point.name: - angle2 = math.radians(m.get("angle")) - #point.magnitude = math.cos(angle1) * m.get("magnitude") - self.outstation.apply_update(opendnp3.Analog(angle2), point.index) - - - elif "value" in m.keys(): - for point in self.outstation.get_agent().point_definitions.all_points(): - if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): - point.value = m.get("value") - self.outstation.apply_update(opendnp3.Binary(point.value), point.index) - except Exception as e: - message_str = "An error occurred while trying to translate the message received" + str(e) - # create_message_updates is called in new_start_service.py (in the new on_message method). def create_message_updates(self, simulation_id, message): """ This method creates an atomic "updates" object for any outstation to consume via their .Apply method. @@ -125,7 +68,6 @@ def create_message_updates(self, simulation_id, message): """ try: message_str = 'received message ' + str(message) - builder = asiodnp3.UpdateBuilder() json_msg = yaml.safe_load(str(message)) @@ -193,13 +135,19 @@ def create_message_updates(self, simulation_id, message): angle2 = math.radians(m.get("angle")) builder.Update(opendnp3.Analog(angle2), point.index) + OesTester.print_solarpanel_output_measurements(point, m) + elif "value" in m.keys(): for point in self.outstation.get_agent().point_definitions.all_points(): - if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value"): - point.value = m.get("value") - builder.Update(opendnp3.Binary(point.value), point.index) - print("==========", point.value, point.index, point.measurement_id) - + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value") and point.group != 30: + point.value = m.get("value") + builder.Update(opendnp3.Binary(point.value), point.index) + if m.get("measurement_mrid") == point.measurement_id and point.value != m.get("value") and point.group == 30: + point.magnitude = m.get("value") + builder.Update(opendnp3.Analog(point.magnitude), point.index) + + OesTester.print_switch_position(point, m) + OesTester.print_voltageregulator_output_measurements(point, m) # Return the atomic "updates" object print("Updates Created") return builder.Build() @@ -276,18 +224,18 @@ def _create_dnp3_object_map(self): reclosers = list() energyconsumers = list() - # Added sorting on MRID to maintain Index Orders for dnp3 index generation between different containers + # Added sorting on name to maintain Index Orders for dnp3 index generation between different containers for x in feeders: - measurements = sorted(x.get("measurements", []), key=lambda x: x['mRID'], reverse=True) - capacitors = sorted(x.get("capacitors", []), key=lambda x: x['mRID'], reverse=True) - regulators = sorted(x.get("regulators", []), key=lambda x: x['mRID'], reverse=True) - switches = sorted(x.get("switches", []), key=lambda x: x['mRID'], reverse=True) - solarpanels = sorted(x.get("solarpanels", []), key=lambda x: x['mRID'], reverse=True) - batteries = sorted(x.get("batteries", []), key=lambda x: x['mRID'], reverse=True) - fuses = sorted(x.get("fuses", []), key=lambda x: x['mRID'], reverse=True) - breakers = sorted(x.get("breakers", []), key=lambda x: x['mRID'], reverse=True) - reclosers = sorted(x.get("reclosers", []), key=lambda x: x['mRID'], reverse=True) - energyconsumers = sorted(x.get("energyconsumers", []), key=lambda x: x['mRID'], reverse=True) + measurements = sorted(x.get("measurements", []), key=lambda x: (x['name'],x['measurementType'],x["phases"]), reverse=True) + capacitors = sorted(x.get("capacitors", []), key=lambda x: x['name'], reverse=True) + regulators = sorted(x.get("regulators", []), key=lambda x: x['bankName'], reverse=True) + switches = sorted(x.get("switches", []), key=lambda x: x['name'], reverse=True) + solarpanels = sorted(x.get("solarpanels", []), key=lambda x: x['name'], reverse=True) + batteries = sorted(x.get("batteries", []), key=lambda x: x['name'], reverse=True) + fuses = sorted(x.get("fuses", []), key=lambda x: x['name'], reverse=True) + breakers = sorted(x.get("breakers", []), key=lambda x: x['name'], reverse=True) + reclosers = sorted(x.get("reclosers", []), key=lambda x: x['name'], reverse=True) + energyconsumers = sorted(x.get("energyconsumers", []), key=lambda x: x['name'], reverse=True) # Unique grouping of measurements - GroupBy Name, Type and Connectivity node groupByNameTypeConNode = defaultdict(list) @@ -301,24 +249,22 @@ def _create_dnp3_object_map(self): measurement_type = grpM[0].get("measurementType") measurement_id = "" for m in grpM: - measurement_id += m.get("mRID") + "," - #measurement_id = grpM[0].get("mRID") + "," + grpM[1].get("mRID")+ "," + grpM[2].get("mRID") - name1 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VAR-value' - name2 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-Watts-value' - name3 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VA-value' + name1 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VAR-value' + name2 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-Watts-value' + name3 = grpM[0]['name'] + '-' + "Phases:ABC" + '-net-VA-value' - description1 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VAR" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") - description2 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-Watts" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") - description3 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VA" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description1 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VAR" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description2 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-Watts" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") + description3 = "Name:" + grpM[0]['name'] + ",MeasurementType:" + "net-VA" + ",ConnectivityNode:" + grpM[0].get("ConnectivityNode") +",SimObject:" + grpM[0].get("SimObject") - self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) - self.c_ai += 1 - self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) - self.c_ai += 1 - self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) - self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name1, description1, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name2, description2, measurement_type, measurement_id) + self.c_ai += 1 + self.assign_val_a("AI", 30, 1, self.c_ai, name3, description3, measurement_type, measurement_id) + self.c_ai += 1 # Create Each Phase DNP3 Points for m in measurements: @@ -351,12 +297,13 @@ def _create_dnp3_object_map(self): elif m['MeasurementClass'] == "Discrete" and measurement_type == "Pos": if "RatioTapChanger" in m['name'] or "reg" in m["SimObject"]: - # TODO: Do we need step? - for r in range(5, 7): # [r==4]: Step, [r==5]: LineDropR, [r==6]:LineDropX - self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id, attribute[r]) - self.c_ao += 1 + for r in range(0, 7): + description = "Name:" + m['name'] + ",Phase:" + m['phases'] + ",MeasurementType:" + attribute[r] + ",ConnectivityNode:" + m.get("ConnectivityNode") +",SimObject:" + m.get("SimObject") + + self.assign_val_a("AI", 30, 1, self.c_ai, name, description, attribute[r], measurement_id) + self.c_ai += 1 else: - self.assign_val_a("DI", 1, 2, self.c_di, name, description, measurement_type, measurement_id) + self.assign_valc("DI", 1, 2, self.c_di, name, description, measurement_id, measurement_type) self.c_di += 1 for m in capacitors: @@ -385,17 +332,14 @@ def _create_dnp3_object_map(self): description = "Name:" + m['bankName'] + ",ConductingEquipment_type:RatioTapChanger_Reg" +",Phase:" + m['bankPhases'] + ",Attribute:" + reg_attribute[n] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id[0], reg_attribute[n]) self.c_ao += 1 - self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id[0], reg_attribute[n]) - self.c_ai += 1 - for i in range(5, 7): + for i in range(4, 7): for j in range(0, len(m['bankPhases'])): measurement_id = m.get("mRID")[j] name = m['tankName'][j] + '-' + m['bankPhases'][j] description = "Name:" + m['tankName'][j] + ",ConductingEquipment_type:RatioTapChanger_Reg"+ ",Phase:" + m['bankPhases'][j] + ",controlAttribute:" + reg_attribute[i] self.assign_val_d("AO", 42, 3, self.c_ao, name, description, measurement_id,reg_attribute[i]) self.c_ao += 1 - self.assign_val_d("AI", 30, 1, self.c_ai, name, description, measurement_id,reg_attribute[i]) - self.c_ai += 1 + for m in solarpanels: for k in range(0, len(m['phases'])): diff --git a/dnp3/service/dnp3/oestester.py b/dnp3/service/dnp3/oestester.py new file mode 100644 index 0000000..a40b5c6 --- /dev/null +++ b/dnp3/service/dnp3/oestester.py @@ -0,0 +1,53 @@ +from dnp3.points import ( + PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT +) + +test_switch = False +test_solarpanel = False +test_voltageregulator = True + +class OesTester(): + + @staticmethod + def print_switch_position(point: PointDefinition, m:dict): + if not test_switch: + return + + if "LoadBreakSwitch" in point.name: + print("--------------------------------") + print("Description:", point.description) + print("POS:", point.value) + print("MRID:", m.get("measurement_mrid")) + print("--------------------------------") + + @staticmethod + def print_solarpanel_output_measurements(point: PointDefinition, m: dict): + if not test_solarpanel: + return + + if "VAR" not in point.name and "Watts" not in point.name and "angle" not in point.name: + if "PhotovoltaicUnit" in point.name or "pv_pvmtr" in point.description or "pv1" in point.description : + print("--------------------------------") + print("Type:", point.measurement_type) + print("Description:", point.description) + print("Value:", point.value) + print("Magnitude:", point.magnitude) + print("MRID:", point.measurement_id) + print("--------------------------------") + + @staticmethod + def print_voltageregulator_output_measurements(point: PointDefinition, m: dict): + if not test_voltageregulator: + return + #or "vr_ld" in point.description.lower() + if "feeder" in point.description.lower() : + print("--------------------------------") + print("Type:", point.measurement_type) + print("Name:", point.description) + print("Description:", point.description) + print("Value:", point.value) + print("Index:", point.index) + print("Group:", point.group) + print("Magnitude:", point.magnitude) + print("MRID:", point.measurement_id) + print("--------------------------------") \ No newline at end of file diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index f3321be..9ec8011 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -105,8 +105,7 @@ def process_point_value(self, command_type, command, index, op_type): else: _log.debug("command_status={},command_value={}".format(command.status, command.value)) for point in self.outstation.get_agent().point_definitions.all_points(): - # print(command.value, point.attribute) - if point.name in str(point_value.point_def): + if point.name in str(point_value.point_def) and point.index==index: self._diff.clear() self._diff.add_difference(point.measurement_id, point.attribute, command.value, 0) # value : received value msg = self._diff.get_message() @@ -343,7 +342,7 @@ def on_message(simulation_id,message): opts = parser.parse_args() simulation_id = opts.simulation_id - with open("/gridappsd/services/gridappsd-dnp3/service/dnp3/port.json", 'r') as f: + with open("/tmp/port.json", 'r') as f: port_config = json.load(f) print(port_config) From 96c45ab597811ff5838f1f596078b226b8c5c9ab Mon Sep 17 00:00:00 2001 From: jsimpson Date: Mon, 5 Oct 2020 13:44:41 -0600 Subject: [PATCH 27/64] Updated .gitingnore --- .gitignore | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/.gitignore b/.gitignore index 1ab4360..15cfbc1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,111 @@ +__pycache__/ +*.py[cod] +*$py.class \.idea/ docs/build env/ *.swp + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +pydnp3/dnp3/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +*.pytest_cache/ + +# VisualStudio and VisualStudio Code project settings +.vscode/ +.vs/ From e913c510646c7800f1477144ff2ad11b6ff17857 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 30 Oct 2020 16:12:04 -0600 Subject: [PATCH 28/64] Updated to add dnp3_to_cim mapping. Note book to build test dictionaries. Basic test for shark meter. --- .../dnp3/__pycache__/__init__.cpython-37.pyc | Bin 136 -> 136 bytes .../__pycache__/cim_to_dnp3.cpython-37.pyc | Bin 7734 -> 13670 bytes .../__pycache__/outstation.cpython-37.pyc | Bin 15274 -> 16009 bytes .../dnp3/__pycache__/points.cpython-37.pyc | Bin 27747 -> 28289 bytes dnp3/service/dnp3/dnp3_to_cim.py | 63 ++ dnp3/service/dnp3/master.py | 35 +- dnp3/service/new_start_service.py | 15 +- test/MappingExample.ipynb | 562 ++++++++++++++++++ test/master_cmd.py | 191 ++++++ test/model_dict.json | 245 ++++++++ 10 files changed, 1103 insertions(+), 8 deletions(-) create mode 100644 dnp3/service/dnp3/dnp3_to_cim.py create mode 100644 test/MappingExample.ipynb create mode 100644 test/master_cmd.py create mode 100644 test/model_dict.json diff --git a/dnp3/service/dnp3/__pycache__/__init__.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/__init__.cpython-37.pyc index 96d22a14d5dc6683728515c127db0b3adc767d31..bd0161c41796b820dacdad37ab0f7634739d3ebd 100644 GIT binary patch delta 20 ZcmeBR>|o?^;^pOH0D`?Gy73cvY5*r>1X}|o?^;^pOH0D`x7nPMmM)Bpe~O9aON diff --git a/dnp3/service/dnp3/__pycache__/cim_to_dnp3.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/cim_to_dnp3.cpython-37.pyc index a11f2d60d8e8fb74084f298c2a130c16e2b05279..c7a09c285d3f8b2ff1c23176153a615ad206debb 100644 GIT binary patch literal 13670 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 diff --git a/dnp3/service/dnp3/__pycache__/outstation.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/outstation.cpython-37.pyc index 3e599e68de23da5c6b799531e3c019853d08ea60..826a4c497c810b3df8e4bb259d04bfd04b5321b1 100644 GIT binary patch delta 5067 zcmb6dTWlN0@%DIkJW?b@QW8ZyEKx7ZbYl4-`4J^iV%f2+#+DO5>V#-~p}8lSG+)^} zITb^hh~(5w(wDJm8>DSY76{q~L4ZI_f##z{fEGx8@>8Hd4k?PDXwl{)&0pKJ1v;}w zOAeD52!)&7ot@d8otd4P)yt1Ry%L%Y2K^HJW}bV(kRQArir0P!BPvmnmr`y^Qbdg9 zlnkw$_Y{k>7&6*GJD?axcIH=ad4F(7SIQ-3J6#rkvbU2z8oWsS z{OzC-IRVGDVgRIYh-gHU`JJJ=YqPQ>*;1WQf(v5LhD~ywHFvDkRa?t>>pJ46xoy|*D9mL563B3I>HpRoLY zt0dQNZ-Mc^8tIW%JbH0hOosTC_CfL|?f4 z-&zGz=mA1b{c;vvdHG%zq!dxc5uYOUmx@*l*!+e3oe&V4|Y^Hq2TO zSCF&ZT_=4!5|8Z_DpgBMHaJ3y_CnwchL}m)*;4TxnGU| zu&QKD#wZPr>)}6*|L;f&R=4cpJV@CGlilUb0VcxGGBd-3<-&m``%EHsJf~2CvTB z`d=H-8}cl~qsJ%&pI|*ieHi1b1j8sqIS-uCzy7EGACNt`R+2dCQn_rhr0mEOyBuP* zZ1J)Gt~|JXrc@};{D;3XU_>7PHpe@E^31Wh`J;c=V8r8t+ccDz4f7*|iMt$ap~MPl zo8gHZEpJ{n^AZaHwkcK>z4JyLkCFPBRG%pvI%}|c!+fHpAYXG{-j2o$WhaV z8=uc+gaggZ%}4pi!_V|@3i)X}^7=@ejPPHLtPb6Z40d*U_`qln*~TxAK6~pWw~P&u z9sHiLKOWjxQmB(XiU&voNPb!n+#z}{%?hVX5k*deX(5B}0UaEu;mNV668`@k=!mFK z>aq>qu|{$V)C44_HbgyOYjtk}yr}AdR7|RJ1MUa(eSlMz2OtIbA&<6I6_GOn6q9if z@6b7^IHgE-=#rm3L!=dzNR^n~o(tC_qgebjWUEqDSIIf7zSI@ui~V35tse5ik>%)A=RSa`dzFb7#)nclykc`DAp{br>4gQS9Zi z>F9=mK4Tb;Dwz27W=f1&iEJ@pUo;b&v2xr>z)8{<(w3QcJZoPR(`ma@$Yv75 z?kTO|MCn4#%-H1Ajxwu}Hx#3~4$K!Emf!*R_ctmJr`eC!HNwHPVp z8mV;;(5_@*%^(G3IZDD{Duqf&>*sBT?cvT5n@qRBJ2PmzB9{O!r(BF~5*iphqFyoi~L9YZjO0COojjzFkKYT%;ybbQ%@jc7ZPFdA)@1L(6M#_(0dgb z{=k(LIWVfv>0VveRruNf`t^QY(<9KsKTVJ8U4lCUjN*6wpFwgj|Hb6UD1~wzULqRS z=g@j7)a20W;$`9ke+{r%{<*y3Hm&x+%F<*XS?UYYPf=s z(^DBhe*UkC@x(3IJogppResOT87~q+m|S1o`5@6yMo2^N?&=%IwB>lgLksC572#&n zIIdNm0g9)2-|mAq*5K*gF+~AU*5BG4CP&@HaK&c<+$h|>fd~=3yrr^%k+F5%Zvw@0 z8wc|7!O7ul($8#UTtjdb!BYr?Cw?7!Pa{By7_zgkArM+_Dq+)^&`6hYdAw*k$Qw|E zv$P5GUH>q~*C%(98UE+V{4Gp>e~am7a6=)EuyeDF{Cu0ggc1mw%OU>y)RZ?43xFN? zhf}ZKyQ#TLxa7WfPM4{fU|mo#ghYS^+1pd}qs ze%)_Fs(0V<)C(6@=kM=7zqQhFFq1LMwn-E4wkbkdfQq{ka9>H7xJyt#2l(uPJ+;j! zDT=VqQoH+7a#X1L*)n?$&+|P5&m-7yFmWLq&+*dYuWxvkB=^E2McX|ip7w?^WEWC|Qc&_y+i%#dX&4IR#LHNl- zY8I}9i{GSmA(eQZ>UR|&>*vjx5umVo0pQb8hv%f<6Rk-#=|psUv@6=F2Xyj(Y#=qy delta 4514 zcmbVPYit|G5x%`U9*<9v6fIFtQ!m?09LtjBmuefb71vhc&~fZm$sy^5)}AQRe0lHW z*eqoxmg6)I&?ecWNs%BS(LY5WK??=>RTyZ40xcS#0g4vr0sWT(MgKJa3bYB(nWaK0 zO5y?~akG1~v$M0av){};zw+a!!Y4u@zXZScZykK%^#@)GC+e@j8?NmQZgO^K?OlJ~DFMgTY-ssTp>P7pXD;CQJH z9Np6LVc>KC$4C9Z@mp#>0-Pv~259i6WW+3;hAzqMMJx8woizE7he-D#l@0AVX_1Jf zbi>q? z@4Tw-P=OU%drL2?B+SF1A8gq|e<{%*+ZJPZD|6LOUz*%M3^E>zWOw{ zqPr>O6OEpxbiedOX+;+Dp+;{_n2s*X_t7w%^CvZu?{@~^eQ=fZNGl3AJG`WWXFImn zKLBpBrkF#}4nsQ(?Fh6Zmp_=yF(f#lA{9@+`EkBfcYTL;!&cqZVvuL=G)Cxw!@P_j=JpymL)1%#~BI7qwJO${^-H6=LL!?b%9+(#vdt3sYokF}W`_?W$&> zoOM0ay11O>uf?WE*)|9+SIJrq3t=yM@`_b*PAo4jTCBAk8{i+ue(Kd0%dD7j_#2%@ zXBZ^|_opA;_fV#k$y$tca6O*vjUYdEyi%E6%9Ki0;Y`kUtP%`I`T6*8zz4%DfF#DR z#0NJ|>#}H#u9v0`ni}%(0ak#rpF!o)K?PQo!gs%N45DD&qN_t5UZbKLR3)!Hbt3grwnD;bt4T8_o+pY!3a-=th+pr2iZ7_|WpU}iPJCwe>iue)C$13aDR+K zPkGs~w=U|2;mA!fl5uk7()*}oef=LKCi&6sUjEO-CkN6g!UkY$-4;U=Q%YORVJAW4 z_ozY`qVw_I+ug!tq_u@jC(1wU?G62@Evr;Vc{4Gv7VqmK2gH=tt_^={C39ghyLGi7 z%y!!veEiA&UyJ~1od9UKOySyx8eyt8JT;|85a9qy6u3dYwW)zO25ZT z$ie7jk0HV7%s|o(CK2MFY#-a<`rrI8$Xr&ht`_R4-Sp(5|Q_L ztKHaaxj-L*%MI7#{BosIW)9)M+%d4$96Ce-!blBm80Q}i50N<6M#cl1RPdRRF5>0$ zBe(lEtR$@%-z@g&NP;AI*XWI*yNSUvL$NoCoRw?|+ zGHCBbAP{$7y5;!{D}H{RyJ&szhcsj=G9>2WyazHd$!krK zg#m%R0P{K)4UiZV7*j5(?2O~1o`!g9%B$oCar})yQ(94LY9m-9kf3CUDYX#-p66N& zw1iKCnvR+WgKb_aO{tO-x#VZ>5NQQUp=y^C&BtIFkzDP!YT6Al52X&(SG4Qeqtati zNq!XGMUcL>^GV`2^i1iR?Enn(f9={=XFaf?Tx6#tRZkpu9IIGyl1@1Z7?8{)uVf0# zRubSjnJZE2DjcKZ#I~zC*D98)nGsmHL(5NQA3cy~K1th`{^wpqbS#}wh8QUqGKUV!jorbI=+ZFsKbvgcs-BLB<2 z<1M{>XsU~xm^~6e3Vc;JJ#j z4$333LVFoEbhZRh;#gQhX02Pia%gJqX%OG!F>w#Si@cji+6q3iS-5KkhOVQzXo0S% za!_G2ZC4%P2M%ZdPgfnmMgOO(UftxX=tI}el(SGLqk~F02QD}|k?)y~k`sJpdRB#N zr176je@lTV;p1PMtZ!f#rg5wtpm9A=@3Cv_WwhWGB;QBUc7Ojg*Y{Fx)K#UMpN;ZqH)f7?v$Px9`g z1N~kBZ!MJG@F89qR_6~M9XPYaS!ywpEBz3669E}X#$@7$j|KcN@DuhX@GuRclxae; ztRlk;1zZ-swfK8S?<0f!qob3T@pihAxvd{QGsq6ify++=Dit8fiReMKn~rTG`t>8l jGF>iM$3{RxeECC$lM|X>iPfdLl#1<&#bTX$hfe+r{Wis? diff --git a/dnp3/service/dnp3/__pycache__/points.cpython-37.pyc b/dnp3/service/dnp3/__pycache__/points.cpython-37.pyc index 2769fa2baf8bf3965908a5e66a725ea99c11c29d..222e48df5f275e3f6b1f470e80fed4b144f72bf8 100644 GIT binary patch delta 3052 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 diff --git a/dnp3/service/dnp3/dnp3_to_cim.py b/dnp3/service/dnp3/dnp3_to_cim.py new file mode 100644 index 0000000..ea5a493 --- /dev/null +++ b/dnp3/service/dnp3/dnp3_to_cim.py @@ -0,0 +1,63 @@ +import json +import pandas as pd + +def build_conversion(DNP3_device_xlsx): + df = pd.read_excel(r'DNP3 list.xlsx', sheet_name='Shark') + conversion_dict = {} + df = df.set_index('Index') + shark_dict = df.T.to_dict() + conversion_dict['Shark'] = shark_dict + conversion_dict = {"Shark": shark_dict} + with open("conversion_dict.json", "w") as f: + json.dump(conversion_dict, f, indent=2) + +def model_line_dict(model_dict_json): + from_node = '632' + to_node = '633' + node_name = '633' + line_name = from_node + to_node + + with open("model_dict.json") as f: + model_dict = json.load(f) + + model_line_dict = {from_node + to_node: {}} + device_type = "Shark" + if device_type == 'Shark': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('ACLineSegment_' + line_name): + print(meas) + if meas['measurementType'] == 'PNV': + model_line_dict[from_node + to_node]['Voltage feedback ' + meas['phases'] + ' (L-N)'] = { + 'mrid': meas['mRID'], 'type': 'magnitude'} + model_line_dict[from_node + to_node]['Voltage feedback ' + meas['phases'] + ' (L-L)'] = { + 'mrid': meas['mRID'], 'type': 'angle'} + if meas['measurementType'] == 'VA': + model_line_dict[from_node + to_node]['P ' + meas['phases']] = {'mrid': meas['mRID'], + 'type': 'magnitude'} + model_line_dict[from_node + to_node]['Q ' + meas['phases']] = {'mrid': meas['mRID'], + 'type': 'angle'} + # if meas['ConnectivityNode'] == node_name and meas['ConductingEquipment_type'] == 'ACLineSegment': + # print(meas) + if device_type == 'Capacitor': + pass + if device_type == 'Regulator': + pass + model_line_dict + with open("model_line_dict.json", "w") as f: + json.dump(model_line_dict, f, indent=2) + +class CIMMapping(): + """ This creates dnp3 input and output points for incoming CIM messages and model dictionary file respectively.""" + + def __init__(self, conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json"): + with open(conversion_dict) as f: + # json.dump(shark_dict,f) + conversion_dict = json.load(f) + self.conversion_dict = conversion_dict + + with open(model_line_dict) as f: + model_line_dict = json.load(f) + self.model_line_dict = model_line_dict + +if __name__ == '__main__': + CIMMapping() \ No newline at end of file diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 92ece83..6a20846 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -78,7 +78,8 @@ class MyMaster: def __init__(self, HOST="192.168.1.2", # "127.0.0.1 LOCAL= "0.0.0.0", - PORT=2000, + PORT=20000, + DNP3_ADDR=1, log_handler=asiodnp3.ConsoleLogger().Create(), listener=asiodnp3.PrintingChannelListener().Create(), soe_handler=asiodnp3.PrintingSOEHandler().Create(), @@ -106,12 +107,15 @@ def __init__(self, self.stack_config = asiodnp3.MasterStackConfig() self.stack_config.master.responseTimeout = openpal.TimeDuration().Seconds(2) self.stack_config.link.RemoteAddr = 1024 ## TODO get from config Was 10 + self.stack_config.link.RemoteAddr = DNP3_ADDR + print('') _log.debug('Adding the master to the channel.') self.soe_handler = soe_handler self.master_application = master_application self.master = self.channel.AddMaster("master", - asiodnp3.PrintingSOEHandler().Create(), + # asiodnp3.PrintingSOEHandler().Create(), + self.soe_handler, self.master_application, self.stack_config) @@ -239,7 +243,10 @@ class SOEHandler(opendnp3.ISOEHandler): This is an interface for SequenceOfEvents (SOE) callbacks from the Master stack to the application layer. """ - def __init__(self): + def __init__(self, name, device, dnp3_to_cim): + self._name = name + self._device = device + self._dnp3_to_cim = dnp3_to_cim super(SOEHandler, self).__init__() def Process(self, info, values): @@ -262,9 +269,29 @@ def Process(self, info, values): visitor_class = visitor_class_types[type(values)] visitor = visitor_class() values.Foreach(visitor) + + cim_msg = {} + conversion_dict = self._dnp3_to_cim.conversion_dict + model_line_dict = self._dnp3_to_cim.model_line_dict + + element_attr_to_mrid = model_line_dict[self._name] + conversion = conversion_dict[self._device] + import numbers for index, value in visitor.index_and_value: - log_string = 'SOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + print("Jeff SOE", index, value, isinstance(value, numbers.Number)) + log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + if str(index) in conversion: + dnp3_attr_name = conversion[str(index)]['Analog input:'] + if dnp3_attr_name in element_attr_to_mrid: + print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) + if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in cim_msg: + cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, 'angle': 0} + value_type = element_attr_to_mrid[dnp3_attr_name]['type'] + cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value + else: + print(" No entry for index " + str(index)) + print(cim_msg) def Start(self): _log.debug('In SOEHandler.Start') diff --git a/dnp3/service/new_start_service.py b/dnp3/service/new_start_service.py index 50eeda7..c98a355 100755 --- a/dnp3/service/new_start_service.py +++ b/dnp3/service/new_start_service.py @@ -395,15 +395,22 @@ def on_message(simulation_id,message): outstation = start_outstation(oustation, processor) #for outstation in outstation_list: dnp3_object.load_outstation(outstation) - dnp3_object_list.append(dnp3_object) - + #dnp3_object_list.append(dnp3_object)group of matser ips and ports + # { + # HOST = "192.168.1.2", # "127.0.0.1 + # LOCAL = "0.0.0.0", + # PORT = 20000, + # DNP_3_ADDR = 1, + # } + dnp3_to_cim = None + ## TODO Loop through devices if True: app = MyMaster(HOST="192.168.1.2", # "127.0.0.1 LOCAL="0.0.0.0", - PORT=2000, + PORT=20000, log_handler=MyLogger(), listener=AppChannelListener(), - soe_handler=SOEHandler(), + soe_handler=SOEHandler('632633', 'Shark', dnp3_to_cim), master_application=MasterApplication()) app.set_agent(processor) ## HARD CODE diff --git a/test/MappingExample.ipynb b/test/MappingExample.ipynb new file mode 100644 index 0000000..a11bc59 --- /dev/null +++ b/test/MappingExample.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n" + ] + }, + { + "cell_type": "code", + "execution_count": 154, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
IndexAnalog input:MultiplierUnits
01Voltage feedback A (L-N)(150 / 32768)V
12Voltage feedback B (L-N)(150 / 32768)V
23Voltage feedback C (L-N)(150 / 32768)V
34Voltage feedback A (L-L)(300 / 32768)V
45Voltage feedback B (L-L)(300 / 32768)V
56Voltage feedback C (L-L)(300 / 32768)V
67Current feedback (A)(10 / 32768)A
78Current feedback (B)(10 / 32768)A
89Current feedback (C)(10 / 32768)A
910Total 3 ph P(4500 / 32768)W
1011Total 3 ph Q(4500 / 32768)VARs
1112Total 3 ph VA(4500 / 32768)VA
1213PF0.001NaN
1313Hz0.01Hz
1436P A(4500/32768)W
1537P B(4500/32768)W
1638P C(4500/32768)W
1739Q A(4500/32768)VARs
1840Q B(4500/32768)VARs
1941Q C(4500/32768)VARs
\n", + "
" + ], + "text/plain": [ + " Index Analog input: Multiplier Units\n", + "0 1 Voltage feedback A (L-N) (150 / 32768) V\n", + "1 2 Voltage feedback B (L-N) (150 / 32768) V\n", + "2 3 Voltage feedback C (L-N) (150 / 32768) V\n", + "3 4 Voltage feedback A (L-L) (300 / 32768) V\n", + "4 5 Voltage feedback B (L-L) (300 / 32768) V\n", + "5 6 Voltage feedback C (L-L) (300 / 32768) V\n", + "6 7 Current feedback (A) (10 / 32768) A\n", + "7 8 Current feedback (B) (10 / 32768) A\n", + "8 9 Current feedback (C) (10 / 32768) A\n", + "9 10 Total 3 ph P (4500 / 32768) W\n", + "10 11 Total 3 ph Q (4500 / 32768) VARs\n", + "11 12 Total 3 ph VA (4500 / 32768) VA\n", + "12 13 PF 0.001 NaN\n", + "13 13 Hz 0.01 Hz\n", + "14 36 P A (4500/32768) W\n", + "15 37 P B (4500/32768) W\n", + "16 38 P C (4500/32768) W\n", + "17 39 Q A (4500/32768) VARs\n", + "18 40 Q B (4500/32768) VARs\n", + "19 41 Q C (4500/32768) VARs" + ] + }, + "execution_count": 154, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.read_excel (r'DNP3 list.xlsx', sheet_name='Shark')\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 155, + "metadata": {}, + "outputs": [], + "source": [ + "conversion_dict = {}\n", + "df = df.set_index('Index')" + ] + }, + { + "cell_type": "code", + "execution_count": 156, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jsimpson/.conda/envs/go_solar36/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: DataFrame columns are not unique, some columns will be omitted.\n", + " \"\"\"Entry point for launching an IPython kernel.\n" + ] + }, + { + "data": { + "text/plain": [ + "{1: {'Analog input:': 'Voltage feedback A (L-N)',\n", + " 'Multiplier': '(150 / 32768) ',\n", + " 'Units': 'V'},\n", + " 2: {'Analog input:': 'Voltage feedback B (L-N)',\n", + " 'Multiplier': '(150 / 32768) ',\n", + " 'Units': 'V'},\n", + " 3: {'Analog input:': 'Voltage feedback C (L-N)',\n", + " 'Multiplier': '(150 / 32768) ',\n", + " 'Units': 'V'},\n", + " 4: {'Analog input:': 'Voltage feedback A (L-L)',\n", + " 'Multiplier': '(300 / 32768) ',\n", + " 'Units': 'V'},\n", + " 5: {'Analog input:': 'Voltage feedback B (L-L)',\n", + " 'Multiplier': '(300 / 32768) ',\n", + " 'Units': 'V'},\n", + " 6: {'Analog input:': 'Voltage feedback C (L-L)',\n", + " 'Multiplier': '(300 / 32768) ',\n", + " 'Units': 'V'},\n", + " 7: {'Analog input:': 'Current feedback (A)',\n", + " 'Multiplier': '(10 / 32768) ',\n", + " 'Units': 'A'},\n", + " 8: {'Analog input:': 'Current feedback (B)',\n", + " 'Multiplier': '(10 / 32768) ',\n", + " 'Units': 'A'},\n", + " 9: {'Analog input:': 'Current feedback (C)',\n", + " 'Multiplier': '(10 / 32768) ',\n", + " 'Units': 'A'},\n", + " 10: {'Analog input:': 'Total 3 ph P',\n", + " 'Multiplier': '(4500 / 32768) ',\n", + " 'Units': 'W'},\n", + " 11: {'Analog input:': 'Total 3 ph Q',\n", + " 'Multiplier': '(4500 / 32768) ',\n", + " 'Units': 'VARs'},\n", + " 12: {'Analog input:': 'Total 3 ph VA',\n", + " 'Multiplier': '(4500 / 32768) ',\n", + " 'Units': 'VA'},\n", + " 13: {'Analog input:': 'Hz', 'Multiplier': 0.01, 'Units': 'Hz'},\n", + " 36: {'Analog input:': 'P A', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", + " 37: {'Analog input:': 'P B', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", + " 38: {'Analog input:': 'P C', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", + " 39: {'Analog input:': 'Q A', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", + " 40: {'Analog input:': 'Q B', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", + " 41: {'Analog input:': 'Q C', 'Multiplier': '(4500/32768)', 'Units': 'VARs'}}" + ] + }, + "execution_count": 156, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "shark_dict = df.T.to_dict()\n", + "conversion_dict['Shark'] = shark_dict\n", + "shark_dict" + ] + }, + { + "cell_type": "code", + "execution_count": 157, + "metadata": {}, + "outputs": [], + "source": [ + "conversion_dict = {\"Shark\":shark_dict}\n", + "with open(\"conversion_dict.json\",\"w\") as f:\n", + " json.dump(conversion_dict,f,indent=2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 158, + "metadata": {}, + "outputs": [], + "source": [ + "elements_to_device = {'632633':'Shark'}" + ] + }, + { + "cell_type": "code", + "execution_count": 159, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'name': 'ACLineSegment_632633_Power', 'mRID': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C', 'measurementType': 'VA', 'phases': 'C', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '632', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Power', 'mRID': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C', 'measurementType': 'VA', 'phases': 'A', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '632', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Voltage', 'mRID': '_5bc367a5-f771-494a-83bd-b4560d538df7', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_F0E10483-D8AD-46BE-AF5F-08228955796B', 'measurementType': 'PNV', 'phases': 'C', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '633', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Voltage', 'mRID': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_F0E10483-D8AD-46BE-AF5F-08228955796B', 'measurementType': 'PNV', 'phases': 'A', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '633', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Voltage', 'mRID': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_F0E10483-D8AD-46BE-AF5F-08228955796B', 'measurementType': 'PNV', 'phases': 'B', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '633', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Power', 'mRID': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C', 'measurementType': 'VA', 'phases': 'B', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '632', 'SimObject': 'line_632633'}\n" + ] + } + ], + "source": [ + "import json\n", + "from_node = '632'\n", + "to_node = '633'\n", + "node_name='633'\n", + "line_name = from_node+to_node\n", + "\n", + "with open(\"model_dict.json\") as f:\n", + " model_dict = json.load(f)\n", + "\n", + "model_line_dict={from_node+to_node:{}}\n", + "device_type = \"Shark\"\n", + "if device_type == 'Shark':\n", + " for meas in model_dict['feeders'][0]['measurements']:\n", + " if meas['name'].startswith('ACLineSegment_'+line_name):\n", + " print(meas)\n", + " if meas['measurementType'] == 'PNV':\n", + " model_line_dict[from_node+to_node]['Voltage feedback '+meas['phases']+ ' (L-N)'] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[from_node+to_node]['Voltage feedback '+meas['phases']+ ' (L-L)'] = {'mrid':meas['mRID'],'type':'angle'}\n", + " if meas['measurementType'] == 'VA':\n", + " model_line_dict[from_node+to_node]['P '+meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[from_node+to_node]['Q '+meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " # if meas['ConnectivityNode'] == node_name and meas['ConductingEquipment_type'] == 'ACLineSegment':\n", + " # print(meas)\n", + "if device_type == 'Capacitor':\n", + " pass\n", + "if device_type == 'Regulator':\n", + " pass\n", + "model_line_dict\n", + "with open(\"model_line_dict.json\",\"w\") as f:\n", + " json.dump(model_line_dict,f,indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 132, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"conversion_dict.json\") as f:\n", + "# json.dump(shark_dict,f)\n", + " conversion_dict = json.load(f)\n", + "\n", + "with open(\"model_line_dict.json\") as f:\n", + " model_line_dict = json.load(f)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 147, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_806d5de7-541b-4944-8825-c7afffa5abc2': {'magnitude': 28008, 'angle': 0.11},\n", + " '_bd5f3945-5d16-4694-896e-92da8a0e4895': {'magnitude': 0, 'angle': 0},\n", + " '_5bc367a5-f771-494a-83bd-b4560d538df7': {'magnitude': 0, 'angle': 0},\n", + " '_4044e33a-ecc2-4d3d-8584-f65643c4eea2': {'magnitude': 200, 'angle': 0.12},\n", + " '_ebd287b8-bceb-4471-a709-c82fbd34bd30': {'magnitude': 0, 'angle': 0},\n", + " '_11bf885c-b677-42ba-859c-d1a013a9f36e': {'magnitude': 0, 'angle': 0}}" + ] + }, + "execution_count": 147, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "index = 1\n", + "values = {1:28008,2:0,3:0,4:0.11,5:0,6:0,36:200,37:0,38:0,39:0.12,40:0,41:0}\n", + "cim_msg = {}\n", + "\n", + "for name, device in element_to_device.items():\n", + " model = model_line_dict[name]\n", + " conversion = conversion_dict[device]\n", + "\n", + " for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", + " value = values[index]\n", + " meas_type = conversion[str(index)]['Analog input:']\n", + "# print(meas_type,model[meas_type])\n", + " if model[meas_type]['mrid'] not in cim_msg:\n", + " cim_msg[model[meas_type]['mrid']] = {'magnitude':0,'angle':0}\n", + " value_type = model[meas_type]['type']\n", + " cim_msg[model[meas_type]['mrid']][value_type] = value\n", + " # for type_name, mrid in model.items():\n", + " # print(conversion[index],type_name)\n", + "cim_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 139, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'1': {'Analog input:': 'Voltage feedback A (L-N)',\n", + " 'Multiplier': '(150 / 32768) ',\n", + " 'Units': 'V'},\n", + " '2': {'Analog input:': 'Voltage feedback B (L-N)',\n", + " 'Multiplier': '(150 / 32768) ',\n", + " 'Units': 'V'},\n", + " '3': {'Analog input:': 'Voltage feedback C (L-N)',\n", + " 'Multiplier': '(150 / 32768) ',\n", + " 'Units': 'V'},\n", + " '4': {'Analog input:': 'Voltage feedback A (L-L)',\n", + " 'Multiplier': '(300 / 32768) ',\n", + " 'Units': 'V'},\n", + " '5': {'Analog input:': 'Voltage feedback B (L-L)',\n", + " 'Multiplier': '(300 / 32768) ',\n", + " 'Units': 'V'},\n", + " '6': {'Analog input:': 'Voltage feedback C (L-L)',\n", + " 'Multiplier': '(300 / 32768) ',\n", + " 'Units': 'V'},\n", + " '7': {'Analog input:': 'Current feedback\\xa0(A)',\n", + " 'Multiplier': '(10 / 32768) ',\n", + " 'Units': 'A'},\n", + " '8': {'Analog input:': 'Current feedback\\xa0(B)',\n", + " 'Multiplier': '(10 / 32768) ',\n", + " 'Units': 'A'},\n", + " '9': {'Analog input:': 'Current feedback\\xa0(C)',\n", + " 'Multiplier': '(10 / 32768) ',\n", + " 'Units': 'A'},\n", + " '10': {'Analog input:': 'Total 3 ph P',\n", + " 'Multiplier': '(4500 / 32768) ',\n", + " 'Units': 'W'},\n", + " '11': {'Analog input:': 'Total 3 ph Q',\n", + " 'Multiplier': '(4500 / 32768) ',\n", + " 'Units': 'VARs'},\n", + " '12': {'Analog input:': 'Total 3 ph VA',\n", + " 'Multiplier': '(4500 / 32768) ',\n", + " 'Units': 'VA'},\n", + " '13': {'Analog input:': 'Hz', 'Multiplier': 0.01, 'Units': 'Hz'},\n", + " '36': {'Analog input:': 'P A', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", + " '37': {'Analog input:': 'P B', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", + " '38': {'Analog input:': 'P C', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", + " '39': {'Analog input:': 'Q A', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", + " '40': {'Analog input:': 'Q B', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", + " '41': {'Analog input:': 'Q C', 'Multiplier': '(4500/32768)', 'Units': 'VARs'}}" + ] + }, + "execution_count": 139, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversion = conversion_dict[device]\n", + "conversion" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'P C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'magnitude'},\n", + " 'Q C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'angle'},\n", + " 'P A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'magnitude'},\n", + " 'Q A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'Voltage feedback C (L-N)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback C (L-L)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'Voltage feedback A (L-N)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback A (L-L)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", + " 'type': 'angle'},\n", + " 'Voltage feedback B (L-N)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback B (L-L)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", + " 'type': 'angle'},\n", + " 'P B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'magnitude'},\n", + " 'Q B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}}" + ] + }, + "execution_count": 101, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/test/master_cmd.py b/test/master_cmd.py new file mode 100644 index 0000000..f05ccb5 --- /dev/null +++ b/test/master_cmd.py @@ -0,0 +1,191 @@ +import cmd +import logging +import sys + +sys.path.append("../service") + +from datetime import datetime +from pydnp3 import opendnp3, openpal +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.master import command_callback, restart_callback +from dnp3.dnp3_to_cim import CIMMapping + + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) + + +class MasterCmd(cmd.Cmd): + """ + Create a DNP3Manager that acts as the Master in a DNP3 Master/Outstation interaction. + + Accept command-line input that sends commands and data to the Outstation, + using the line-oriented command interpreter framework from the 'cmd' Python Standard Library. + """ + + def __init__(self): + cmd.Cmd.__init__(self) + self.prompt = 'master> ' # Used by the Cmd framework, displayed when issuing a command-line prompt. + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json") + elements_to_device = {'632633': 'Shark'} + self.application = MyMaster(HOST="127.0.0.1",# "127.0.0.1 + LOCAL="0.0.0.0", + PORT=20000, + DNP3_ADDR=10, + log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler('632633', 'Shark', dnp3_to_cim), + master_application=MasterApplication()) + + def startup(self): + """Display the command-line interface's menu and issue a prompt.""" + print('Welcome to the DNP3 master request command line. Supported commands include:') + self.do_menu('') + self.cmdloop('Please enter a command.') + exit() + + def do_menu(self, line): + """Display a menu of command-line options. Command syntax is: menu""" + print('\tchan_log_all\tSet the channel log level to ALL_COMMS.') + print('\tchan_log_normal\tSet the channel log level to NORMAL.') + print('\tdisable_unsol\tPerform the function DISABLE_UNSOLICITED.') + print('\thelp\t\tDisplay command-line help.') + print('\tmast_log_all\tSet the master log level to ALL_COMMS.') + print('\tmast_log_normal\tSet the master log level to NORMAL.') + print('\tmenu\t\tDisplay this menu.') + print('\to1\t\tSend a DirectOperate LATCH_ON command.') + print('\to2\t\tSend a DirectOperate analog value.') + print('\to3\t\tSend a DirectOperate CommandSet.') + print('\tquit') + print('\trestart\t\tRequest an outstation cold restart.') + print('\ts1\t\tSend a SelectAndOperate LATCH_ON command.') + print('\ts2\t\tSend a SelectAndOperate CommandSet.') + print('\tscan_all\tRead data from the outstation (ScanAllObjects).') + print('\tscan_fast\tDemand immediate execution of the fast (every 1 mins) Class 1 scan.') + print('\tscan_range\tPerform an ad-hoc scan (ScanRange) of GroupVariation 1.2, range 0..3.') + print('\tscan_slow\tDemand immediate execution of the slow (every 30 mins) All-Classes scan.') + print('\twrite_time\tWrite a TimeAndInterval to the outstation.') + + def do_chan_log_all(self, line): + """Set the channel log level to ALL_COMMS. Command syntax is: chan_log_all""" + self.application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) + + def do_chan_log_normal(self, line): + """Set the channel log level to NORMAL. Command syntax is: chan_log_normal""" + self.application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.NORMAL)) + print('Channel log filtering level is now: {0}'.format(opendnp3.levels.NORMAL)) + + def do_disable_unsol(self, line): + """Perform the function DISABLE_UNSOLICITED. Command syntax is: disable_unsol""" + headers = [opendnp3.Header().AllObjects(60, 2), + opendnp3.Header().AllObjects(60, 3), + opendnp3.Header().AllObjects(60, 4)] + self.application.master.PerformFunction("disable unsolicited", + opendnp3.FunctionCode.DISABLE_UNSOLICITED, + headers, + opendnp3.TaskConfig().Default()) + + def do_mast_log_all(self, line): + """Set the master log level to ALL_COMMS. Command syntax is: mast_log_all""" + self.application.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + _log.debug('Master log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) + + def do_mast_log_normal(self, line): + """Set the master log level to NORMAL. Command syntax is: mast_log_normal""" + self.application.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.NORMAL)) + _log.debug('Master log filtering level is now: {0}'.format(opendnp3.levels.NORMAL)) + + def do_o1(self, line): + """Send a DirectOperate BinaryOutput (group 12) index 5 LATCH_ON to the Outstation. Command syntax is: o1""" + self.application.send_direct_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 5, + command_callback) + + def do_o2(self, line): + """Send a DirectOperate AnalogOutput (group 41) index 10 value 7 to the Outstation. Command syntax is: o2""" + self.application.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), + 10, + command_callback) + + def do_o3(self, line): + """Send a DirectOperate BinaryOutput (group 12) CommandSet to the Outstation. Command syntax is: o3""" + self.application.send_direct_operate_command_set(opendnp3.CommandSet( + [ + opendnp3.WithIndex(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), 0), + opendnp3.WithIndex(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), 1) + ]), + command_callback + ) + + # This could also have been in multiple steps, as follows: + # command_set = opendnp3.CommandSet() + # command_set.Add([ + # opendnp3.WithIndex(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), 0), + # opendnp3.WithIndex(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), 1) + # ]) + # self.application.send_direct_operate_command_set(command_set, command_callback) + + def do_restart(self, line): + """Request that the Outstation perform a cold restart. Command syntax is: restart""" + self.application.master.Restart(opendnp3.RestartType.COLD, restart_callback) + + def do_s1(self, line): + """Send a SelectAndOperate BinaryOutput (group 12) index 8 LATCH_ON to the Outstation. Command syntax is: s1""" + self.application.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 8, + command_callback) + + def do_s2(self, line): + """Send a SelectAndOperate BinaryOutput (group 12) CommandSet to the Outstation. Command syntax is: s2""" + self.application.send_select_and_operate_command_set(opendnp3.CommandSet( + [ + opendnp3.WithIndex(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), 0) + ]), + command_callback + ) + + def do_scan_all(self, line): + """Call ScanAllObjects. Command syntax is: scan_all""" + self.application.master.ScanAllObjects(opendnp3.GroupVariationID(2, 1), opendnp3.TaskConfig().Default()) + + def do_scan_fast(self, line): + """Demand an immediate fast scan. Command syntax is: scan_fast""" + self.application.fast_scan.Demand() + + def do_scan_range(self, line): + """Do an ad-hoc scan of a range of points (group 1, variation 2, indexes 0-3). Command syntax is: scan_range""" + self.application.master.ScanRange(opendnp3.GroupVariationID(1, 2), 0, 3, opendnp3.TaskConfig().Default()) + + def do_scan_slow(self, line): + """Demand an immediate slow scan. Command syntax is: scan_slow""" + self.application.slow_scan.Demand() + + def do_write_time(self, line): + """Write a TimeAndInterval to the Outstation. Command syntax is: write_time""" + millis_since_epoch = int((datetime.now() - datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) + self.application.master.Write(opendnp3.TimeAndInterval(opendnp3.DNPTime(millis_since_epoch), + 100, + opendnp3.IntervalUnits.Seconds), + 0, # index + opendnp3.TaskConfig().Default()) + + def do_quit(self, line): + """Quit the command-line interface. Command syntax is: quit""" + self.application.shutdown() + exit() + + +def main(): + cmd_interface = MasterCmd() + _log.debug('Initialization complete. In command loop.') + cmd_interface.startup() + _log.debug('Exiting.') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/test/model_dict.json b/test/model_dict.json new file mode 100644 index 0000000..dd1e874 --- /dev/null +++ b/test/model_dict.json @@ -0,0 +1,245 @@ +{"feeders":[ +{"name":"ieee13nodeckt", +"mRID":"_49AD8E07-3BF9-A4E2-CB8F-C3722F837B62", +"substation":"IEEE13", +"substationID":"_6C62C905-6FC7-653D-9F1E-1340F974A587", +"subregion":"Small", +"subregionID":"_ABEB635F-729D-24BF-B8A4-E2EF268D8B9E", +"region":"IEEE", +"regionID":"_73C512BD-7249-4F50-50DA-D93849B89C43", +"synchronousmachines":[ +], +"capacitors":[ +{"name":"cap1","mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","CN1":"675","phases":"ABC","kvar_A":200.0,"kvar_B":200.0,"kvar_C":200.0,"nominalVoltage":4160.0,"nomU":4160.0,"phaseConnection":"Y","grounded":true,"enabled":false,"mode":null,"targetValue":0.0,"targetDeadband":0.0,"aVRDelay":0.0,"monitoredName":null,"monitoredClass":null,"monitoredBus":null,"monitoredPhase":null}, +{"name":"cap2","mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","CN1":"611","phases":"C","kvar_A":0.0,"kvar_B":0.0,"kvar_C":100.0,"nominalVoltage":4160.0,"nomU":2400.0,"phaseConnection":"Y","grounded":true,"enabled":false,"mode":null,"targetValue":0.0,"targetDeadband":0.0,"aVRDelay":0.0,"monitoredName":null,"monitoredClass":null,"monitoredBus":null,"monitoredPhase":null} +], +"regulators":[ +{"bankName":"Reg","size":"3","bankPhases":"ABC","tankName":["reg1","reg2","reg3"],"endNumber":[2,2,2],"endPhase":["A","B","C"],"rtcName":["reg1","reg2","reg3"],"mRID":["_6CFE3829-3905-42FD-A9AD-5C70D3D835EF","_C03B9EC2-DD5F-4C39-A4A7-E212CBBC9940","_5B8EA691-0051-4CF2-9A03-140171317E4D"],"monitoredPhase":["A","B","C"],"TapChanger.tculControlMode":["volt","volt","volt"],"highStep":[16,16,16],"lowStep":[-16,-16,-16],"neutralStep":[0,0,0],"normalStep":[0,0,0],"TapChanger.controlEnabled":[true,true,true],"lineDropCompensation":[true,true,true],"ltcFlag":[true,true,true],"RegulatingControl.enabled":[true,true,true],"RegulatingControl.discrete":[true,true,true],"RegulatingControl.mode":["voltage","voltage","voltage"],"step":[10,8,11],"targetValue":[122.0000,122.0000,122.0000],"targetDeadband":[2.0000,2.0000,2.0000],"limitVoltage":[0.0000,0.0000,0.0000],"stepVoltageIncrement":[0.6250,0.6250,0.6250],"neutralU":[2400.0000,2400.0000,2400.0000],"initialDelay":[15.0000,15.0000,15.0000],"subsequentDelay":[2.0000,2.0000,2.0000],"lineDropR":[3.0000,3.0000,3.0000],"lineDropX":[9.0000,9.0000,9.0000],"reverseLineDropR":[0.0000,0.0000,0.0000],"reverseLineDropX":[0.0000,0.0000,0.0000],"ctRating":[700.0000,700.0000,700.0000],"ctRatio":[3500.0000,3500.0000,3500.0000],"ptRatio":[20.0000,20.0000,20.0000]} +], +"solarpanels":[ +{"name":"house","mRID":"_1E58BE0E-7D37-46A5-A8A2-04B56C78E50A","CN1":"house","phases":"BS","ratedS":5000.0,"ratedU":208.0,"p":4000.000,"q":0.000,"maxIFault":1.111}, +{"name":"school","mRID":"_15EEE173-CC90-4888-B9B8-43916D56890C","CN1":"634","phases":"ABC","ratedS":300000.0,"ratedU":480.0,"p":240000.000,"q":0.000,"maxIFault":1.111} +], +"batteries":[ +{"name":"house","mRID":"_D26D2D54-A267-4FC0-B86F-5FC93983784C","CN1":"house","phases":"BS","ratedS":5000.0,"ratedU":208.0,"p":-5000.0,"q":0.0,"ratedE":13500.0,"storedE":5000.0,"batteryState":"Charging","maxIFault":1.111}, +{"name":"school","mRID":"_9E931F3B-0D9D-4CBE-B771-9400202B20E8","CN1":"634","phases":"ABC","ratedS":100000.0,"ratedU":480.0,"p":-100000.0,"q":0.0,"ratedE":200000.0,"storedE":100000.0,"batteryState":"Charging","maxIFault":1.111} +], +"switches":[ +{"name":"671692","mRID":"_517413CB-6977-46FA-8911-C82332E42884","CN1":"671","CN2":"692","phases":"ABC","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false}, +{"name":"sect1","mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","CN1":"684","CN2":"tap","phases":"C","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false} +], +"fuses":[ +{"name":"fuse1","mRID":"_43EF8365-F932-409B-A51E-FBED3F6DFFAA","CN1":"633","CN2":"xf1","phases":"ABC","ratedCurrent":100.0,"breakingCapacity":0.0,"normalOpen":false} +], +"sectionalisers":[ +], +"breakers":[ +{"name":"brkr1","mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","CN1":"650","CN2":"brkr","phases":"ABC","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false} +], +"reclosers":[ +{"name":"rec1","mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","CN1":"632","CN2":"mid","phases":"ABC","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false} +], +"disconnectors":[ +], +"energyconsumers":[ +{"name":"611","mRID":"_60B55036-DD71-40CE-ADDA-97B8CE7447DC","phases":"C"}, +{"name":"634a","mRID":"_C39149DE-3451-4D33-B4C2-B1E6C6FC9AAB","phases":"A"}, +{"name":"634b","mRID":"_B21C5599-1D00-4FCF-904B-58D9D4CAC49A","phases":"B"}, +{"name":"634c","mRID":"_3B2021A7-4BFC-418D-9C20-BD6838E52CF8","phases":"C"}, +{"name":"645","mRID":"_47E52220-4596-4AF0-9724-0167B72A4DB8","phases":"B"}, +{"name":"646","mRID":"_0BC9183A-9067-4E06-AA5B-48DC2AB30C80","phases":"B"}, +{"name":"652","mRID":"_572BCFC9-E1D6-4419-9A33-EDD284806AF3","phases":"A"}, +{"name":"670a","mRID":"_91A37B4C-C38B-4269-8467-F40870934386","phases":"A"}, +{"name":"670b","mRID":"_70188FF4-04F0-48D2-8706-698D1FD086E6","phases":"B"}, +{"name":"670c","mRID":"_E503BBF8-3C82-4DF7-B6E9-2488ECFCC847","phases":"C"}, +{"name":"671","mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","phases":"ABC"}, +{"name":"675a","mRID":"_9CAAF741-BE1E-4893-8044-1E507B7DDC38","phases":"A"}, +{"name":"675b","mRID":"_8E34333C-2BB8-4631-B072-383F1CA38F60","phases":"B"}, +{"name":"675c","mRID":"_3FFFB3F7-A7AF-49E1-ACEE-449FC73D3CD6","phases":"C"}, +{"name":"692","mRID":"_8CA46F28-84C0-4FE2-B228-EC4080777865","phases":"C"}, +{"name":"house","mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","phases":"s2:s1"} +], +"measurements":[ +{"name":"Breaker_brkr1_State","mRID":"_00ff72f5-628c-462b-bdd1-2dcc1bd519b5","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"Recloser_rec1_Voltage","mRID":"_017f359e-77e5-48ca-9a02-eaa59d14a941","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"Breaker_brkr1_State","mRID":"_04d9f780-ad0c-4205-b94d-531e66087f2d","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_0769c269-2a4f-4e30-a5ae-fa30f7dc271b","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_169CB0D6-0002-457F-9594-7FEB09DA102D","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"692","SimObject":"swt_671692"}, +{"name":"EnergyConsumer_692","mRID":"_0793bcc6-eab5-45d1-891a-973379c5cdec","ConductingEquipment_mRID":"_8CA46F28-84C0-4FE2-B228-EC4080777865","Terminal_mRID":"_9CA4064A-AACD-49DB-9296-7ADC7E195358","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"692","ConnectivityNode":"692","SimObject":"ld_692"}, +{"name":"EnergyConsumer_675a","mRID":"_0c2e8ddb-6043-4721-a276-27fc19e86c04","ConductingEquipment_mRID":"_9CAAF741-BE1E-4893-8044-1E507B7DDC38","Terminal_mRID":"_02A67D2E-58FF-4CAB-9A85-4346E9E08F1B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675a","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_692675_Power","mRID":"_0cffa585-68ec-478f-b38d-28903fb7a5c6","ConductingEquipment_mRID":"_7060D0BB-B30D-4932-8FA1-40820A0FC4D0","Terminal_mRID":"_B4E1784C-5500-47AE-958B-A286CE8C17A5","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"692675","ConnectivityNode":"692","SimObject":"line_692675"}, +{"name":"ACLineSegment_671684_Power","mRID":"_0e3593f1-b1ee-443c-a350-3d89fb29c51e","ConductingEquipment_mRID":"_D34B0D01-B082-4081-A3CC-B68B9B8313A4","Terminal_mRID":"_A4C58654-2972-4A3E-AE65-3868941DA037","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671684","ConnectivityNode":"671","SimObject":"line_671684"}, +{"name":"EnergyConsumer_675b","mRID":"_0f40449d-eacd-4929-8972-1b02646a1ab3","ConductingEquipment_mRID":"_8E34333C-2BB8-4631-B072-383F1CA38F60","Terminal_mRID":"_0A29AEF0-5F49-47AE-A413-9308AA61B72B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675b","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_632645_Voltage","mRID":"_110eb93f-4227-4411-91b7-01c284484423","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_4891537F-4088-48F5-9B1A-8294D50901E4","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"645","SimObject":"line_632645"}, +{"name":"ACLineSegment_632633_Power","mRID":"_11bf885c-b677-42ba-859c-d1a013a9f36e","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"632","SimObject":"line_632633"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_13e6fb7e-d41c-4366-a97c-f33543b4b801","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_645646_Power","mRID":"_1408fa1f-9dc3-4ea9-9240-ddfac7f23864","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_F3C0F2D0-4D28-49B5-A920-37409B99229D","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"645","SimObject":"line_645646"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_1550b167-a168-44ef-ba67-8e029f22b433","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_1671d766-d683-4038-bf23-86ecf752381d","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_BB0411D7-5261-433D-B327-549DA536EEEC","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_611","mRID":"_17dd8a60-a58d-4acf-b19a-4c9b1c7a1c80","ConductingEquipment_mRID":"_60B55036-DD71-40CE-ADDA-97B8CE7447DC","Terminal_mRID":"_2F5C9D9B-8CC5-4636-8BC8-2AC78F01F5BC","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"611","ConnectivityNode":"611","SimObject":"ld_611"}, +{"name":"LoadBreakSwitch_671692_State","mRID":"_18ccedff-3c2c-4c1b-891e-5af708e4421c","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"LoadBreakSwitch_671692_Current","mRID":"_1a0ef275-fbdd-46b3-acdd-de643f6168ab","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"A","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"EnergyConsumer_634b","mRID":"_1bcc8391-d8ce-4c32-9131-163ff1eada50","ConductingEquipment_mRID":"_B21C5599-1D00-4FCF-904B-58D9D4CAC49A","Terminal_mRID":"_272D015C-A83B-457C-8109-5E78E92CB6E4","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634b","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_671","mRID":"_1c575353-e825-4bab-8ca1-04f64602244c","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_1c8b757a-e27a-496b-bfa8-fbd19c2a3292","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"ACLineSegment_632645_Power","mRID":"_23b10aed-23da-4911-8be7-55e1d722d4cf","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_A563A3A5-EA39-4998-96B2-538227E2D2F6","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"632","SimObject":"line_632645"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_243fdef8-003d-4d71-89cf-0f5e1a2e1562","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_BB0411D7-5261-433D-B327-549DA536EEEC","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_2a54cdf8-d469-4ac7-aee9-e49b39d46756","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_650632_Voltage","mRID":"_2e09270d-ba65-4243-ac20-7501f63c9bf0","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"ACLineSegment_650632_Power","mRID":"_2f6db582-9ee9-469b-85ea-ad75e0f9fd7d","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_3064b38f-e8ae-4d40-aa81-fb5c4d52704a","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"Breaker_brkr1_Current","mRID":"_309d665e-b3dd-46b1-9a94-366628cabc42","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"A","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"EnergyConsumer_671","mRID":"_327bce10-4686-43c6-8cde-56306d151b15","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"ACLineSegment_671680_Power","mRID":"_32de2bad-ad0c-4686-9869-94ea00616138","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_8BE8D3EA-EEE9-406C-B24C-B169C8EC799A","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"671","SimObject":"line_671680"}, +{"name":"ACLineSegment_645646_Voltage","mRID":"_32ed617f-263f-4bfe-93ac-a189e0e5d171","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_D02B9E99-3557-48F5-B475-4871E47D08F6","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"646","SimObject":"line_645646"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_3324240e-a64c-4acd-9ff9-db9347e9a250","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_670b","mRID":"_373565da-5ef8-490e-8b42-4a29686c8427","ConductingEquipment_mRID":"_70188FF4-04F0-48D2-8706-698D1FD086E6","Terminal_mRID":"_CCC4E540-46CC-422C-98DA-32898FD31637","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670b","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_37780e7f-0a56-4980-96ac-32670b6145cd","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"EnergyConsumer_house","mRID":"_379cc9ba-b6d7-4739-b8b4-7fbe8768834a","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"PNV","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"EnergyConsumer_671","mRID":"_3892f276-5fcb-44cc-ab7c-1cbb272af2fd","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"Breaker_brkr1_Current","mRID":"_392f7130-efe9-493c-bd42-3a31a556b20d","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"A","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_39615e17-18ee-4304-a0ce-1be657b08602","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerTransformer_xfm1_Voltage","mRID":"_3a304c71-d5ec-4bc1-b118-8f2ace903adc","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"Recloser_rec1_State","mRID":"_3a4d201c-bc7e-4d65-91bc-37e1ea8e7125","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_675c","mRID":"_3ae108c0-5e36-443f-9616-98182c646665","ConductingEquipment_mRID":"_3FFFB3F7-A7AF-49E1-ACEE-449FC73D3CD6","Terminal_mRID":"_72362309-A950-40BC-A38C-E08E1AD53477","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675c","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_3fe5535c-8533-4c43-a741-ade81f143cc6","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"PNV","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"ACLineSegment_632633_Power","mRID":"_4044e33a-ecc2-4d3d-8584-f65643c4eea2","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"632","SimObject":"line_632633"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_448ef334-0751-4a27-b168-c9a9bf5d4cb4","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_692675_Power","mRID":"_45fa2ec2-756b-4d4f-aab8-41d2f9dc8a73","ConductingEquipment_mRID":"_7060D0BB-B30D-4932-8FA1-40820A0FC4D0","Terminal_mRID":"_B4E1784C-5500-47AE-958B-A286CE8C17A5","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"692675","ConnectivityNode":"692","SimObject":"line_692675"}, +{"name":"LinearShuntCompensator_cap2","mRID":"_47c95834-3913-406c-82ba-08374f1b02d2","ConductingEquipment_mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","Terminal_mRID":"_FD3834FF-6C3B-434F-A41D-77EB8237F53D","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap2","ConnectivityNode":"611","SimObject":"cap_cap2"}, +{"name":"PowerTransformer_xfm1_Voltage","mRID":"_48aa1067-ee1d-4763-9845-3ad80be775a0","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"EnergyConsumer_652","mRID":"_4c5466c3-71b6-4491-96f2-e2fac89301ca","ConductingEquipment_mRID":"_572BCFC9-E1D6-4419-9A33-EDD284806AF3","Terminal_mRID":"_02217298-AD5B-463C-A912-957740214701","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"652","ConnectivityNode":"652","SimObject":"ld_652"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_4ea65778-8d96-41b8-9352-00c63d0f337a","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_645","mRID":"_5142fd4b-8371-40b1-85e4-938e372d64a7","ConductingEquipment_mRID":"_47E52220-4596-4AF0-9724-0167B72A4DB8","Terminal_mRID":"_15298D41-D5E5-464F-A96D-01FFCF0DA7DB","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"645","ConnectivityNode":"645","SimObject":"ld_645"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_5220d83e-ee9b-451d-8ce7-2768b6ad8612","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"Recloser_rec1_Voltage","mRID":"_53d39eb5-4602-454f-9fd1-66540399cc39","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_634c","mRID":"_5479191e-e5bc-4d11-ba9e-f210121ac087","ConductingEquipment_mRID":"_3B2021A7-4BFC-418D-9C20-BD6838E52CF8","Terminal_mRID":"_C61A2238-FE09-4406-B172-26CB9F145DB1","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634c","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_675b","mRID":"_55b04aca-c331-4a1d-ad1f-f34ff9188396","ConductingEquipment_mRID":"_8E34333C-2BB8-4631-B072-383F1CA38F60","Terminal_mRID":"_0A29AEF0-5F49-47AE-A413-9308AA61B72B","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675b","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_632670_Voltage","mRID":"_55f6b5ef-d9b6-489b-ac11-4fac69aad17c","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_B554744A-F5B5-485C-B67E-2487A8740295","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"670","SimObject":"line_632670"}, +{"name":"EnergyConsumer_675c","mRID":"_56ec1537-6ded-4851-8c45-ad8bacde74b0","ConductingEquipment_mRID":"_3FFFB3F7-A7AF-49E1-ACEE-449FC73D3CD6","Terminal_mRID":"_72362309-A950-40BC-A38C-E08E1AD53477","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675c","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_632633_Voltage","mRID":"_5bc367a5-f771-494a-83bd-b4560d538df7","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_F0E10483-D8AD-46BE-AF5F-08228955796B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"633","SimObject":"line_632633"}, +{"name":"Recloser_rec1_State","mRID":"_5db0c160-f3f0-488e-88a9-5c8ef4ee130a","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"LoadBreakSwitch_671692_Current","mRID":"_5e915fa7-a0c2-4d38-8a7d-2de423b396e7","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"LoadBreakSwitch_sect1_Current","mRID":"_62ee3f81-c7c6-439a-89fe-8fe30daa3bff","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_8F517DFE-D985-4D24-8339-96FA2A789E88","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"684","SimObject":"swt_sect1"}, +{"name":"EnergyConsumer_634c","mRID":"_632cbbe7-057c-4106-9197-2c70f709e5a5","ConductingEquipment_mRID":"_3B2021A7-4BFC-418D-9C20-BD6838E52CF8","Terminal_mRID":"_C61A2238-FE09-4406-B172-26CB9F145DB1","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634c","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_634a","mRID":"_634b1b30-e4b4-4ab6-9b19-3d5153df2313","ConductingEquipment_mRID":"_C39149DE-3451-4D33-B4C2-B1E6C6FC9AAB","Terminal_mRID":"_D0BA8E68-F269-4382-B1D0-8EA25663C9AA","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634a","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_670c","mRID":"_63dc721e-038b-4068-ab4c-8cf2b5e1aef5","ConductingEquipment_mRID":"_E503BBF8-3C82-4DF7-B6E9-2488ECFCC847","Terminal_mRID":"_F421B562-1AF9-42C1-8906-D80CABA91FC4","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670c","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"EnergyConsumer_670a","mRID":"_65398be0-9abe-44d6-9ef7-359238a371e8","ConductingEquipment_mRID":"_91A37B4C-C38B-4269-8467-F40870934386","Terminal_mRID":"_83481773-E9FC-4720-89B4-796C5081D518","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670a","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"ACLineSegment_684611_Power","mRID":"_65d1b173-43d6-4927-b495-bb61f11518c5","ConductingEquipment_mRID":"_8E180773-2A9B-4136-BC9A-132A52C8E276","Terminal_mRID":"_EA5FF2C4-F7E1-436D-BDEC-1C48D4DBB50D","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"684611","ConnectivityNode":"tap","SimObject":"line_684611"}, +{"name":"ACLineSegment_632670_Power","mRID":"_66bb7d46-33ff-41e1-b58c-9b404e0ab21b","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_5EDB2918-D669-457C-9F44-E08EBA5AC850","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"mid","SimObject":"line_632670"}, +{"name":"Recloser_rec1_Voltage","mRID":"_6e2444b6-606a-4ab6-9db1-29dec0be69bc","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_DD4717B2-5FCD-4E67-9BC8-DD307B80AFEA","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"mid","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_634a","mRID":"_6e304d52-985a-4cb7-8b38-7783f935d975","ConductingEquipment_mRID":"_C39149DE-3451-4D33-B4C2-B1E6C6FC9AAB","Terminal_mRID":"_D0BA8E68-F269-4382-B1D0-8EA25663C9AA","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634a","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_6e76eb6e-363c-4d5d-b304-2e216c9451c0","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_6e929a30-05fc-4237-beb4-8c22613d6b9b","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_704cfb22-8d38-4f2e-8fc6-4f27fa5af2a0","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"EnergyConsumer_645","mRID":"_711494f2-af31-4b64-bc60-876f364fbb74","ConductingEquipment_mRID":"_47E52220-4596-4AF0-9724-0167B72A4DB8","Terminal_mRID":"_15298D41-D5E5-464F-A96D-01FFCF0DA7DB","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"645","ConnectivityNode":"645","SimObject":"ld_645"}, +{"name":"Recloser_rec1_Current","mRID":"_72ccaae8-52e9-4237-86ec-1e5bc4dc328f","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"A","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_7746f8fa-be50-4100-970e-4f19d8597eb8","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_169CB0D6-0002-457F-9594-7FEB09DA102D","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"692","SimObject":"swt_671692"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_7772ef3a-ca5c-44a8-9fc9-c88d318e851c","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"EnergyConsumer_670b","mRID":"_7e26bf88-6547-423e-ad35-4ee2bcf69402","ConductingEquipment_mRID":"_70188FF4-04F0-48D2-8706-698D1FD086E6","Terminal_mRID":"_CCC4E540-46CC-422C-98DA-32898FD31637","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670b","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"ACLineSegment_632670_Power","mRID":"_7e7b8d30-4b84-4305-bfd7-de577f9d98c7","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_5EDB2918-D669-457C-9F44-E08EBA5AC850","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"mid","SimObject":"line_632670"}, +{"name":"LoadBreakSwitch_671692_State","mRID":"_7ed3d06a-a849-4032-a1ef-10d89688ff33","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"ACLineSegment_632633_Voltage","mRID":"_806d5de7-541b-4944-8825-c7afffa5abc2","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_F0E10483-D8AD-46BE-AF5F-08228955796B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"633","SimObject":"line_632633"}, +{"name":"RatioTapChanger_Reg","mRID":"_824c02fc-18d1-4064-b313-22f9f6ce472b","ConductingEquipment_mRID":"_67B57539-590B-4158-9CBB-9DBA2FE6C1F0","Terminal_mRID":"_5A1120EE-0F2A-4698-9FF5-0F0F4F507BAF","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"Reg","ConnectivityNode":"rg60","SimObject":"reg_Reg"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_828ec85a-475f-4836-8371-66ad57d52a2c","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"VA","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_8502efde-b8ce-47b4-afde-b9661485bc0f","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_85c76d73-8662-41f7-a22b-eb1a9e44c0e6","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"ACLineSegment_632670_Voltage","mRID":"_87a43339-1324-40c3-807c-f9f601839c71","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_B554744A-F5B5-485C-B67E-2487A8740295","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"670","SimObject":"line_632670"}, +{"name":"ACLineSegment_684652_Power","mRID":"_8a558465-50bf-4b1b-95b7-2a11c228fe25","ConductingEquipment_mRID":"_ABF877D7-DAC2-4BF0-AB58-9A8A02E92EB3","Terminal_mRID":"_3313C5A7-ED81-47D3-8114-AD654A5DE21B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"684652","ConnectivityNode":"684","SimObject":"line_684652"}, +{"name":"EnergyConsumer_671","mRID":"_8aef86ba-9013-4992-ba27-763b68cc2b2a","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"ACLineSegment_645646_Voltage","mRID":"_8b549f6b-9c61-419f-8315-5af53d6d1fcb","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_D02B9E99-3557-48F5-B475-4871E47D08F6","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"646","SimObject":"line_645646"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_8e729069-0abb-426c-99b0-1d71491d51dd","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"LoadBreakSwitch_sect1_Voltage","mRID":"_8f6a24ef-83d6-4207-8419-0e076350c51d","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_02AE202B-D91B-4EAD-8EC9-3B87BDD67C8B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"tap","SimObject":"swt_sect1"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_9164bc46-2b05-4270-ae85-3d4bb96d6701","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_2847E06B-C8ED-41E6-B515-C61C9E8EB4B4","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"brkr","SimObject":"swt_brkr1"}, +{"name":"LinearShuntCompensator_cap2","mRID":"_91c1c7d7-e0f5-44ce-9986-2042a4e6a29e","ConductingEquipment_mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","Terminal_mRID":"_FD3834FF-6C3B-434F-A41D-77EB8237F53D","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap2","ConnectivityNode":"611","SimObject":"cap_cap2"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_91fbcb9d-a87c-4d5c-b2b0-e9342c91fcb0","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_E1763FE3-82C2-4434-9424-72DF850D982E","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"634","SimObject":"xf_xfm1"}, +{"name":"EnergyConsumer_670a","mRID":"_93e3dd38-7963-427a-a69a-dac60506ceee","ConductingEquipment_mRID":"_91A37B4C-C38B-4269-8467-F40870934386","Terminal_mRID":"_83481773-E9FC-4720-89B4-796C5081D518","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670a","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"EnergyConsumer_646","mRID":"_9555f572-c16e-4492-9c7a-b297195f1980","ConductingEquipment_mRID":"_0BC9183A-9067-4E06-AA5B-48DC2AB30C80","Terminal_mRID":"_579A122E-A262-4830-88C6-1ADDB81761B1","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"646","ConnectivityNode":"646","SimObject":"ld_646"}, +{"name":"ACLineSegment_671680_Power","mRID":"_99d5cba2-4faf-40a9-ac4c-2e27d29b41fa","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_8BE8D3EA-EEE9-406C-B24C-B169C8EC799A","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"671","SimObject":"line_671680"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_9b6d0140-9fc7-4fb0-ada1-ae0ba8d0d822","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"PNV","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_9c51e7a7-3fb7-4bf2-a9f5-6c7aa4330549","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_9d38c431-a309-4835-824e-ce86464ca9ca","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_670671_Power","mRID":"_9e3dfe03-12b4-4643-a17e-0c2158004a51","ConductingEquipment_mRID":"_45395C84-F20A-4F5A-977F-B80348256421","Terminal_mRID":"_EEF69C71-5A87-4378-8DF4-4578635C81B4","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"670671","ConnectivityNode":"670","SimObject":"line_670671"}, +{"name":"Recloser_rec1_Voltage","mRID":"_9e521176-ce36-4e35-9cd8-9c571a809e87","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_DD4717B2-5FCD-4E67-9BC8-DD307B80AFEA","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"mid","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_652","mRID":"_a1d5af7d-1cda-4aa4-bb97-b1add95b94b2","ConductingEquipment_mRID":"_572BCFC9-E1D6-4419-9A33-EDD284806AF3","Terminal_mRID":"_02217298-AD5B-463C-A912-957740214701","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"652","ConnectivityNode":"652","SimObject":"ld_652"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_a36aab82-53b4-41d0-9161-42b31f8e67a2","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_BB0411D7-5261-433D-B327-549DA536EEEC","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650","SimObject":"xf_sub3"}, +{"name":"ACLineSegment_645646_Power","mRID":"_a497b2aa-65f8-42bb-bfcf-cdcf7897c2da","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_F3C0F2D0-4D28-49B5-A920-37409B99229D","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"645","SimObject":"line_645646"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_a5dca590-c8e8-4c09-afc0-ddf636d945bb","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"ACLineSegment_650632_Power","mRID":"_a6a91863-2e4a-4bd1-98f2-a31a12daebfb","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"ACLineSegment_632645_Voltage","mRID":"_a6cf3a73-cf4b-440a-a7f9-90bd41a55778","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_4891537F-4088-48F5-9B1A-8294D50901E4","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"645","SimObject":"line_632645"}, +{"name":"EnergyConsumer_house","mRID":"_a84b5965-6160-4482-aebf-faf0f2534f89","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"VA","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_a8cf16f8-1240-4438-a416-ea7d0e1f7065","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"ACLineSegment_670671_Power","mRID":"_a8e42300-16e7-4024-be50-4a7dc46857cd","ConductingEquipment_mRID":"_45395C84-F20A-4F5A-977F-B80348256421","Terminal_mRID":"_EEF69C71-5A87-4378-8DF4-4578635C81B4","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"670671","ConnectivityNode":"670","SimObject":"line_670671"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_ab4f355f-2872-4a6d-824b-180ee0002601","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_abb9b6c5-0a60-413f-a8b9-a7ae6c7166a5","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_169CB0D6-0002-457F-9594-7FEB09DA102D","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"692","SimObject":"swt_671692"}, +{"name":"LoadBreakSwitch_671692_State","mRID":"_aed74869-30b6-4a4b-9d1b-5488173e9859","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"PowerTransformer_xfm1_Voltage","mRID":"_aee33dac-79ae-4b41-871f-6009b9a0f389","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_671680_Voltage","mRID":"_b291ff81-6813-4d4f-849f-b3d2bc1ba2e9","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_FF3DE145-0066-4C1F-867C-2A2BF3012EDF","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"680","SimObject":"line_671680"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_b4423af0-dfe0-4524-b5eb-bf18d447909a","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"PNV","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_b49a5b70-fe22-405d-8519-de87c8132c9e","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_E1763FE3-82C2-4434-9424-72DF850D982E","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"634","SimObject":"xf_xfm1"}, +{"name":"LinearShuntCompensator_cap2","mRID":"_b5358da9-c206-4149-84ea-8b996495d574","ConductingEquipment_mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","Terminal_mRID":"_FD3834FF-6C3B-434F-A41D-77EB8237F53D","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap2","ConnectivityNode":"611","SimObject":"cap_cap2"}, +{"name":"LoadBreakSwitch_671692_Current","mRID":"_b60dc6a5-90ae-4c96-bdf0-d4968156e13d","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"A","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"ACLineSegment_671680_Power","mRID":"_ba58f48b-42f3-40b1-a6ce-fa4ae2baddb5","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_8BE8D3EA-EEE9-406C-B24C-B169C8EC799A","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"671","SimObject":"line_671680"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_bbf78ce5-9c07-4843-b2c0-0d94305fb581","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_632633_Voltage","mRID":"_bd5f3945-5d16-4694-896e-92da8a0e4895","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_F0E10483-D8AD-46BE-AF5F-08228955796B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"633","SimObject":"line_632633"}, +{"name":"EnergyConsumer_670c","mRID":"_bea2439d-6beb-4222-9c66-5f5ef80504c5","ConductingEquipment_mRID":"_E503BBF8-3C82-4DF7-B6E9-2488ECFCC847","Terminal_mRID":"_F421B562-1AF9-42C1-8906-D80CABA91FC4","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670c","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"ACLineSegment_671680_Voltage","mRID":"_bf97187c-553c-4816-a2e2-991c6fe63383","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_FF3DE145-0066-4C1F-867C-2A2BF3012EDF","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"680","SimObject":"line_671680"}, +{"name":"Recloser_rec1_Current","mRID":"_bfc5ab56-2fb5-4016-a73e-412708e56077","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"A","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"ACLineSegment_684652_Voltage","mRID":"_c08480ec-4d45-4315-bcd6-eee8a194220d","ConductingEquipment_mRID":"_ABF877D7-DAC2-4BF0-AB58-9A8A02E92EB3","Terminal_mRID":"_8692A804-428A-4637-BD47-69C76F873DAB","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"684652","ConnectivityNode":"652","SimObject":"line_684652"}, +{"name":"EnergyConsumer_house","mRID":"_c1da6db6-8258-460d-868a-751b21cdfdb2","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"PNV","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"LoadBreakSwitch_sect1_State","mRID":"_c24f82e9-d6fa-4837-8ccd-cb5990ab2ad2","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_8F517DFE-D985-4D24-8339-96FA2A789E88","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"684","SimObject":"swt_sect1"}, +{"name":"EnergyConsumer_675a","mRID":"_c38705d2-c899-4e4f-b1b2-41c72b248f52","ConductingEquipment_mRID":"_9CAAF741-BE1E-4893-8044-1E507B7DDC38","Terminal_mRID":"_02A67D2E-58FF-4CAB-9A85-4346E9E08F1B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675a","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"RatioTapChanger_Reg","mRID":"_c389843f-37c3-4c76-8e54-f02b70368492","ConductingEquipment_mRID":"_67B57539-590B-4158-9CBB-9DBA2FE6C1F0","Terminal_mRID":"_02CD5661-C654-43F6-8D1A-0D666070BB81","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"Reg","ConnectivityNode":"rg60","SimObject":"reg_Reg"}, +{"name":"ACLineSegment_650632_Voltage","mRID":"_c4957103-316a-4e6b-b121-b37034c54dfd","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_c4d0cbd6-0087-43b7-b8d3-41215f4486d8","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"ACLineSegment_671684_Power","mRID":"_c4f39806-439c-4143-80bc-bb5c4a28d4fc","ConductingEquipment_mRID":"_D34B0D01-B082-4081-A3CC-B68B9B8313A4","Terminal_mRID":"_A4C58654-2972-4A3E-AE65-3868941DA037","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671684","ConnectivityNode":"671","SimObject":"line_671684"}, +{"name":"ACLineSegment_650632_Voltage","mRID":"_c6335ec4-f447-4e04-ac5a-2053fd9cc7c9","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_ca8de879-89fb-44c4-a8c5-97889e0dbffd","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_671","mRID":"_cb3c7168-1741-4444-94ae-2e4e147cd703","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"Recloser_rec1_Current","mRID":"_cbb0db9a-3cb0-4a2b-a6c1-f20ea121409b","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_ccfb98a6-1db7-4000-9b2b-d44db4fc3859","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_cd5b34ea-8e3e-4807-bf04-3500679f331a","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"VA","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"Breaker_brkr1_State","mRID":"_cdc67714-d8f3-4a95-a3b7-ce4c95d6b9ed","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_ce725f7b-341f-44ee-9f83-dfca4a0dc4ca","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_2847E06B-C8ED-41E6-B515-C61C9E8EB4B4","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"brkr","SimObject":"swt_brkr1"}, +{"name":"Breaker_brkr1_Current","mRID":"_ceb0ca47-3fed-4e17-9668-9ab813d73ede","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_d0cf9cd9-bc5b-4665-a39c-3b47b3a7fa88","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_d10a93da-687a-496c-8406-3b5166ee3479","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_d43b62ec-137c-4887-92f2-5536062954bb","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_d6aabda3-6dcd-4f8e-b9d5-314c112e1bc9","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_671684_Voltage","mRID":"_d6bd533e-889d-4c2d-a349-89f56f886363","ConductingEquipment_mRID":"_D34B0D01-B082-4081-A3CC-B68B9B8313A4","Terminal_mRID":"_BC623628-BD23-4A1A-A406-15CA63556149","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671684","ConnectivityNode":"684","SimObject":"line_671684"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_d6bd74bb-8d1f-464e-80aa-b5e96e27a0fa","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_d9c3d91b-6ac2-4927-aaa7-16b85b2c5241","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_2847E06B-C8ED-41E6-B515-C61C9E8EB4B4","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"brkr","SimObject":"swt_brkr1"}, +{"name":"LoadBreakSwitch_sect1_Voltage","mRID":"_da33e6e4-b008-4a45-938b-008c9255c49e","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_8F517DFE-D985-4D24-8339-96FA2A789E88","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"684","SimObject":"swt_sect1"}, +{"name":"ACLineSegment_632670_Power","mRID":"_dabc5297-9462-4faf-80a4-768042918a41","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_5EDB2918-D669-457C-9F44-E08EBA5AC850","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"mid","SimObject":"line_632670"}, +{"name":"ACLineSegment_632645_Power","mRID":"_db63dc39-157b-4f71-b239-38880b5fcc8d","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_A563A3A5-EA39-4998-96B2-538227E2D2F6","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"632","SimObject":"line_632645"}, +{"name":"EnergyConsumer_646","mRID":"_db9565da-e5a1-4829-8f65-7d7bc2619e82","ConductingEquipment_mRID":"_0BC9183A-9067-4E06-AA5B-48DC2AB30C80","Terminal_mRID":"_579A122E-A262-4830-88C6-1ADDB81761B1","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"646","ConnectivityNode":"646","SimObject":"ld_646"}, +{"name":"EnergyConsumer_634b","mRID":"_e03043c2-82dd-4ea2-b635-3642b245a231","ConductingEquipment_mRID":"_B21C5599-1D00-4FCF-904B-58D9D4CAC49A","Terminal_mRID":"_272D015C-A83B-457C-8109-5E78E92CB6E4","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634b","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_e1881b77-3cad-408b-8b1f-2149f3e02577","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"PNV","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"EnergyConsumer_house","mRID":"_e1bb8270-e341-48da-8414-eaaa7d3fd831","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"VA","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_e250f84f-0131-45cb-a9fe-a9f07ec05250","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_e4789bc7-afc2-49a4-8f4f-70b269f0d70d","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_e7659ef6-cd75-4924-822c-8280793cc206","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_e8a3c4f5-e7c7-402e-bfc1-59ddae5bf6a9","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"Recloser_rec1_State","mRID":"_ea48bd8d-4916-4fee-b424-fc967d6a7d90","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_692","mRID":"_ea7f7fe2-21cd-4d00-8d50-0f7a1a53c4e7","ConductingEquipment_mRID":"_8CA46F28-84C0-4FE2-B228-EC4080777865","Terminal_mRID":"_9CA4064A-AACD-49DB-9296-7ADC7E195358","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"692","ConnectivityNode":"692","SimObject":"ld_692"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_ea8aa437-fb7f-4c76-aa69-6419333a28b8","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_E1763FE3-82C2-4434-9424-72DF850D982E","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"634","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_632633_Power","mRID":"_ebd287b8-bceb-4471-a709-c82fbd34bd30","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"632","SimObject":"line_632633"}, +{"name":"ACLineSegment_670671_Power","mRID":"_ed258ce0-a14c-4cdf-8795-800acc5f5ad4","ConductingEquipment_mRID":"_45395C84-F20A-4F5A-977F-B80348256421","Terminal_mRID":"_EEF69C71-5A87-4378-8DF4-4578635C81B4","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"670671","ConnectivityNode":"670","SimObject":"line_670671"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_ed98c9d7-62c1-4c14-b7b8-724e248cdd1b","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"EnergyConsumer_611","mRID":"_edf4deaa-1850-46c8-8f4e-1b3a4dca9615","ConductingEquipment_mRID":"_60B55036-DD71-40CE-ADDA-97B8CE7447DC","Terminal_mRID":"_2F5C9D9B-8CC5-4636-8BC8-2AC78F01F5BC","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"611","ConnectivityNode":"611","SimObject":"ld_611"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_ef2ca369-2fb7-40ba-8d93-e77018e54c37","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"VA","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_f08eb586-8207-4662-a6bb-7ff60f505a55","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_671","mRID":"_f2064797-04d7-4f5d-bf49-e8ce80261cce","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_f4459247-b69b-416a-853e-6021b8747597","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_f500b9cc-932c-44f1-96b6-12ca0a9ba911","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_f6013f90-37e0-4e01-97b4-ea67cf410711","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"Recloser_rec1_Voltage","mRID":"_f6ab2b5f-d5be-4a6c-b8b5-5baa808956de","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"ACLineSegment_650632_Power","mRID":"_f7024843-072d-43ab-ac84-48f6deb5b70f","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"ACLineSegment_692675_Power","mRID":"_f73abf1a-512c-4e81-8623-e8498e368e02","ConductingEquipment_mRID":"_7060D0BB-B30D-4932-8FA1-40820A0FC4D0","Terminal_mRID":"_B4E1784C-5500-47AE-958B-A286CE8C17A5","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"692675","ConnectivityNode":"692","SimObject":"line_692675"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_fa87b12f-dbd6-4004-bcf4-17df5bb388cc","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"VA","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"ACLineSegment_671680_Voltage","mRID":"_fc17a174-ca71-4af1-bb86-c9fda7ec7eca","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_FF3DE145-0066-4C1F-867C-2A2BF3012EDF","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"680","SimObject":"line_671680"}, +{"name":"ACLineSegment_632670_Voltage","mRID":"_fc8c789d-752a-413e-94e6-6bc34728fa5b","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_B554744A-F5B5-485C-B67E-2487A8740295","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"670","SimObject":"line_632670"}, +{"name":"Recloser_rec1_Voltage","mRID":"_fcfe2f82-3062-4f2a-bcfb-0fb7d2b45e8a","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_DD4717B2-5FCD-4E67-9BC8-DD307B80AFEA","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"mid","SimObject":"swt_rec1"}, +{"name":"RatioTapChanger_Reg","mRID":"_fea855f6-4939-4c6d-8d84-02757a82c01d","ConductingEquipment_mRID":"_67B57539-590B-4158-9CBB-9DBA2FE6C1F0","Terminal_mRID":"_F5258B9A-B51E-40DD-9A06-5918E41F3C35","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"Reg","ConnectivityNode":"rg60","SimObject":"reg_Reg"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_ffe351f9-22c0-483d-8017-04dcbb6c107b","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"} +] +}]} From 767d297cb83b08c728d79e434dfccd115bd5f438 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 11 Nov 2020 17:21:05 -0700 Subject: [PATCH 29/64] Updated to handel multiple devices --- dnp3/service/dnp3/dnp3_to_cim.py | 108 +++++++++++++++++++++++++++---- dnp3/service/dnp3/master.py | 31 ++++++++- 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/dnp3/service/dnp3/dnp3_to_cim.py b/dnp3/service/dnp3/dnp3_to_cim.py index ea5a493..eb99aa3 100644 --- a/dnp3/service/dnp3/dnp3_to_cim.py +++ b/dnp3/service/dnp3/dnp3_to_cim.py @@ -1,27 +1,71 @@ import json import pandas as pd -def build_conversion(DNP3_device_xlsx): - df = pd.read_excel(r'DNP3 list.xlsx', sheet_name='Shark') +def get_conversion_model(csv_file,sheet_name): + df = pd.read_excel(csv_file,sheet_name=sheet_name) + master_dict ={ + 'Analog input':{}, + 'Analog output':{}, + 'Binary input':{}, + 'Binary output':{} + } + x = [] + for row in df.iterrows(): + if pd.isna(row[1][2]): + # print(row[0]) + x.append(row[0]) +# print(df.shape) + x.append(df.shape[0]) + it = iter(x) + for x in it: + type_name = df.iloc[x][1] + print(type_name) + next_value = next(it) + print (x+1, next_value) + print(pd.DataFrame(df[x+1:next_value])) + temp_df = pd.DataFrame(df[x+1: next_value]) + temp_df = temp_df.set_index('Index') + master_dict[type_name] = temp_df.T.to_dict() + return master_dict + +def build_conversion(csv_file): conversion_dict = {} - df = df.set_index('Index') - shark_dict = df.T.to_dict() - conversion_dict['Shark'] = shark_dict - conversion_dict = {"Shark": shark_dict} + shark = get_conversion_model(csv_file, sheet_name='Shark') + conversion_dict['Shark'] = shark + shark = get_conversion_model(csv_file, sheet_name='Beckwith CapBank 2') + conversion_dict['Beckwith CapBank'] = shark + shark = get_conversion_model(csv_file, sheet_name='Beckwith LTC') + conversion_dict['Beckwith LTC'] = shark with open("conversion_dict.json", "w") as f: json.dump(conversion_dict, f, indent=2) +# def build_conversion(DNP3_device_xlsx): +# df = pd.read_excel(r'DNP3 list.xlsx', sheet_name='Shark') +# conversion_dict = {} +# df = df.set_index('Index') +# shark_dict = df.T.to_dict() +# conversion_dict['Shark'] = shark_dict +# conversion_dict = {"Shark": shark_dict} +# with open("conversion_dict.json", "w") as f: +# json.dump(conversion_dict, f, indent=2) + def model_line_dict(model_dict_json): from_node = '632' to_node = '633' node_name = '633' line_name = from_node + to_node + line_name = from_node + to_node + cap_name = "cap1" + reg_name = "Reg" with open("model_dict.json") as f: model_dict = json.load(f) - model_line_dict = {from_node + to_node: {}} + model_line_dict = {line_name: {}, + cap_name: {}} device_type = "Shark" + device_type = "Beckwith CapBank 2" + device_type = 'Beckwith LTC' if device_type == 'Shark': for meas in model_dict['feeders'][0]['measurements']: if meas['name'].startswith('ACLineSegment_' + line_name): @@ -38,10 +82,52 @@ def model_line_dict(model_dict_json): 'type': 'angle'} # if meas['ConnectivityNode'] == node_name and meas['ConductingEquipment_type'] == 'ACLineSegment': # print(meas) - if device_type == 'Capacitor': - pass - if device_type == 'Regulator': - pass + elif device_type == 'Beckwith CapBank 2': + # LinearShuntCompensator + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('LinearShuntCompensator_' + cap_name): + if meas['measurementType'] == 'PNV': + # print(meas) + model_line_dict[cap_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], + 'type': 'magnitude'} + model_line_dict[cap_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], 'type': 'angle'} + elif meas['measurementType'] == 'VA': + # print(meas) + model_line_dict[cap_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], + 'type': 'magnitude'} + model_line_dict[cap_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], 'type': 'angle'} + elif meas['measurementType'] == 'POS': + model_line_dict[cap_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], 'type': 'pos'} + pass + for cap in model_dict['feeders'][0]["capacitors"]: + if cap['name'] == cap_name: + model_line_dict[cap_name]['manual close'] = {'mrid': meas['mRID'], 'type': 'magnitude'} + print(cap) + # TODO figure this out + # 0 manual close + # 1 manual open + elif device_type == 'Beckwith LTC': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('LinearShuntCompensator_' + cap_name): + if meas['measurementType'] == 'PNV': + # print(meas) + model_line_dict[reg_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], + 'type': 'magnitude'} + model_line_dict[reg_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], 'type': 'angle'} + elif meas['measurementType'] == 'VA': + # print(meas) + model_line_dict[reg_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], + 'type': 'magnitude'} + model_line_dict[reg_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], 'type': 'angle'} + elif meas['measurementType'] == 'POS': + # Tap position + model_line_dict[cap_name]['? ' + meas['phases'] + ' ?'] = {'mrid': meas['mRID'], 'type': 'pos'} + pass + for reg in model_dict['feeders'][0]["regulators"]: + if reg['bankName'] == reg_name: + # Do I have to count for each position change? + print(reg) + model_line_dict with open("model_line_dict.json", "w") as f: json.dump(model_line_dict, f, indent=2) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 6a20846..748a289 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -33,8 +33,14 @@ # }}} import logging +import numbers import sys import time +import yaml +import json + +from gridappsd.topics import simulation_output_topic, simulation_input_topic +from gridappsd import GridAPPSD, DifferenceBuilder, utils from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 from dnp3.visitors import * @@ -236,6 +242,13 @@ def OnStateChange(self, state): pass + + +def on_message(self, message): + json_msg = yaml.safe_load(str(message)) + print(json_msg) + + class SOEHandler(opendnp3.ISOEHandler): """ Override ISOEHandler in this manner to implement application-specific sequence-of-events behavior. @@ -247,6 +260,10 @@ def __init__(self, name, device, dnp3_to_cim): self._name = name self._device = device self._dnp3_to_cim = dnp3_to_cim + self._gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), + username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + self._gapps.subscribe('/topic/goss.gridappsd.fim.input.'+str(1234), on_message) + super(SOEHandler, self).__init__() def Process(self, info, values): @@ -276,13 +293,18 @@ def Process(self, info, values): element_attr_to_mrid = model_line_dict[self._name] conversion = conversion_dict[self._device] - import numbers + print(conversion) + + ## TODO check for each type seperate binary vs analog + ## Build conversion from xlsx file here instead of notebook + for index, value in visitor.index_and_value: print("Jeff SOE", index, value, isinstance(value, numbers.Number)) log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - if str(index) in conversion: - dnp3_attr_name = conversion[str(index)]['Analog input:'] + if isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: + # dnp3_attr_name = conversion[str(index)]['Analog input:'] + dnp3_attr_name = conversion['Analog input'][str(float(index))]['type'] if dnp3_attr_name in element_attr_to_mrid: print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in cim_msg: @@ -292,6 +314,9 @@ def Process(self, info, values): else: print(" No entry for index " + str(index)) print(cim_msg) + ## '/topic/goss.gridappsd.fim.input' + + self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_msg)) def Start(self): _log.debug('In SOEHandler.Start') From 807c8b4f283ae270f88e31d00f7ff2f2be01765d Mon Sep 17 00:00:00 2001 From: jsimpson Date: Thu, 19 Nov 2020 12:10:07 -0700 Subject: [PATCH 30/64] Added master_main to run a standalone or multiple masters in the future. --- test/master_main.py | 83 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 test/master_main.py diff --git a/test/master_main.py b/test/master_main.py new file mode 100644 index 0000000..f79c5be --- /dev/null +++ b/test/master_main.py @@ -0,0 +1,83 @@ +import os +import sys +sys.path.append("../dnp3/service") + +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.dnp3_to_cim import CIMMapping +from pydnp3 import opendnp3, openpal + +def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json") + elements_to_device = {'632633': 'Shark'} + application_1 = MyMaster(HOST=HOST, # "127.0.0.1 + LOCAL="0.0.0.0", + PORT=int(PORT), + DNP3_ADDR=int(DNP3_ADDR), + log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), + master_application=MasterApplication()) + + masters = [application_1] + # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) + import time + SLEEP_SECONDS = 1 + time.sleep(SLEEP_SECONDS) + # group_variation = opendnp3.GroupVariationID(32, 2) + # time.sleep(SLEEP_SECONDS) + # print('\nReading status 1') + # application_1.master.ScanRange(group_variation, 0, 12) + # time.sleep(SLEEP_SECONDS) + # print('\nReading status 2') + # application_1.master.ScanRange(opendnp3.GroupVariationID(32, 2), 0, 3, opendnp3.TaskConfig().Default()) + # time.sleep(SLEEP_SECONDS) + print('\nReading status 3') + # application_1.slow_scan.Demand() + + # application_1.fast_scan_all.Demand() + + for master in masters: + master.fast_scan_all.Demand() + while True: + cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} + for master in masters: + cim_msg = master.soe_handler._cim_msg + cim_full_msg['messages'].update(cim_msg) + print(cim_full_msg) + time.sleep(60) + + + print('\nStopping') + for master in masters: + master.shutdown() + # application_1.shutdown() + exit() + + # When terminating, it is necessary to set these to None so that + # it releases the shared pointer. Otherwise, python will not + # terminate (and even worse, the normal Ctrl+C won't help). + application_1.master.Disable() + application_1 = None + application_1.channel.Shutdown() + application_1.channel = None + application_1.manager.Shutdown() + +if __name__ == "__main__": + + import argparse + import json + parser = argparse.ArgumentParser() + parser.add_argument("name", help="name of dnp3 outstation") + # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') + args = parser.parse_args() + with open("device_ip_port_config_all.json") as f: + device_ip_port_config_all = json.load(f) + + device_ip_port_dict = device_ip_port_config_all[args.name] + print(device_ip_port_dict) + run_master(device_ip_port_dict['ip'], + device_ip_port_dict['port'], + device_ip_port_dict['link_local_addr'], + device_ip_port_dict['conversion_type'], + '632633') \ No newline at end of file From 993673b1a4a144b531a30422ca792228f612e5d9 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Mon, 23 Nov 2020 14:40:30 -0700 Subject: [PATCH 31/64] Added master_main and outstation_main to test multiple masters and outstations talking to each other. --- dnp3/service/dnp3/master.py | 79 ++- test/MappingExample.ipynb | 1022 ++++++++++++++++++++++++++------- test/build_conversion_dict.py | 11 + test/fim_receiver.py | 28 + test/master_cmd.py | 2 +- test/master_main.py | 65 ++- test/outstation.py | 303 ++++++++++ test/outstation_main.py | 79 +++ test/ports.ipynb | 108 ++++ 9 files changed, 1453 insertions(+), 244 deletions(-) create mode 100644 test/build_conversion_dict.py create mode 100644 test/fim_receiver.py create mode 100644 test/outstation.py create mode 100644 test/outstation_main.py create mode 100644 test/ports.ipynb diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 748a289..918980d 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -135,6 +135,10 @@ def __init__(self, openpal.TimeDuration().Minutes(1), opendnp3.TaskConfig().Default()) + self.fast_scan_all = self.master.AddClassScan(opendnp3.ClassField().AllClasses(), + openpal.TimeDuration().Minutes(1), + opendnp3.TaskConfig().Default()) + # self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) # self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.NOTHING)) @@ -263,9 +267,13 @@ def __init__(self, name, device, dnp3_to_cim): self._gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) self._gapps.subscribe('/topic/goss.gridappsd.fim.input.'+str(1234), on_message) + self._cim_msg = {} super(SOEHandler, self).__init__() + def get_msg(self): + return self._cim_msg + def Process(self, info, values): """ Process measurement data. @@ -287,37 +295,60 @@ def Process(self, info, values): visitor = visitor_class() values.Foreach(visitor) - cim_msg = {} + # cim_msg = {} conversion_dict = self._dnp3_to_cim.conversion_dict model_line_dict = self._dnp3_to_cim.model_line_dict element_attr_to_mrid = model_line_dict[self._name] conversion = conversion_dict[self._device] - print(conversion) + # print(conversion) ## TODO check for each type seperate binary vs analog - ## Build conversion from xlsx file here instead of notebook - - for index, value in visitor.index_and_value: - print("Jeff SOE", index, value, isinstance(value, numbers.Number)) - log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - if isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: - # dnp3_attr_name = conversion[str(index)]['Analog input:'] - dnp3_attr_name = conversion['Analog input'][str(float(index))]['type'] - if dnp3_attr_name in element_attr_to_mrid: - print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) - if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in cim_msg: - cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, 'angle': 0} - value_type = element_attr_to_mrid[dnp3_attr_name]['type'] - cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value - else: - print(" No entry for index " + str(index)) - print(cim_msg) - ## '/topic/goss.gridappsd.fim.input' - - self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_msg)) - + ## Analog check + if type(values) == opendnp3.ICollectionIndexedAnalog: + for index, value in visitor.index_and_value: + print("Jeff SOE", index, value, isinstance(value, numbers.Number)) + log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + if isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: + ## Check if multiplier is na or str + multiplier = 1 + conversion_for_index = conversion['Analog input'][str(float(index))] + if 'Multiplier' in conversion_for_index: + multiplier = conversion_for_index['Multiplier'] + if type(multiplier) != str: + multiplier = conversion_for_index['Multiplier'] + else: + print("No multiplier") + dnp3_attr_name = conversion_for_index['type'] + if dnp3_attr_name in element_attr_to_mrid: + # print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) + if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in self._cim_msg: + self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, + 'angle': 0, + 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} + value_type = element_attr_to_mrid[dnp3_attr_name]['type'] + self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier + else: + print(" No entry for index " + str(index)) + # print(cim_msg) + ## '/topic/goss.gridappsd.fim.input' + elif type(values) == opendnp3.ICollectionIndexedBinaryOutputStatus: + for index, value in visitor.index_and_value: + print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) + log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + else: + for index, value in visitor.index_and_value: + print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) + log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + + # self._cim_msg = cim_msg + # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_msg)) + + # self._cim_msg = {"test":time.time()} + print(self._cim_msg) def Start(self): _log.debug('In SOEHandler.Start') diff --git a/test/MappingExample.ipynb b/test/MappingExample.ipynb index a11bc59..5578f38 100644 --- a/test/MappingExample.ipynb +++ b/test/MappingExample.ipynb @@ -2,16 +2,17 @@ "cells": [ { "cell_type": "code", - "execution_count": 14, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ - "import pandas as pd\n" + "import pandas as pd\n", + "import json\n" ] }, { "cell_type": "code", - "execution_count": 154, + "execution_count": 100, "metadata": {}, "outputs": [ { @@ -36,149 +37,178 @@ " \n", " \n", " Index\n", - " Analog input:\n", + " type\n", " Multiplier\n", + " MultiplierString\n", " Units\n", " \n", " \n", " \n", " \n", " 0\n", - " 1\n", + " NaN\n", + " Analog input\n", + " NaN\n", + " NaN\n", + " NaN\n", + " \n", + " \n", + " 1\n", + " 1.0\n", " Voltage feedback A (L-N)\n", + " 0.004578\n", " (150 / 32768)\n", " V\n", " \n", " \n", - " 1\n", - " 2\n", + " 2\n", + " 2.0\n", " Voltage feedback B (L-N)\n", + " 0.004578\n", " (150 / 32768)\n", " V\n", " \n", " \n", - " 2\n", - " 3\n", + " 3\n", + " 3.0\n", " Voltage feedback C (L-N)\n", + " 0.004578\n", " (150 / 32768)\n", " V\n", " \n", " \n", - " 3\n", - " 4\n", + " 4\n", + " 4.0\n", " Voltage feedback A (L-L)\n", + " 0.009155\n", " (300 / 32768)\n", " V\n", " \n", " \n", - " 4\n", - " 5\n", + " 5\n", + " 5.0\n", " Voltage feedback B (L-L)\n", + " 0.009155\n", " (300 / 32768)\n", " V\n", " \n", " \n", - " 5\n", - " 6\n", + " 6\n", + " 6.0\n", " Voltage feedback C (L-L)\n", + " 0.009155\n", " (300 / 32768)\n", " V\n", " \n", " \n", - " 6\n", - " 7\n", + " 7\n", + " 7.0\n", " Current feedback (A)\n", + " 0.000305\n", " (10 / 32768)\n", " A\n", " \n", " \n", - " 7\n", - " 8\n", + " 8\n", + " 8.0\n", " Current feedback (B)\n", + " 0.000305\n", " (10 / 32768)\n", " A\n", " \n", " \n", - " 8\n", - " 9\n", + " 9\n", + " 9.0\n", " Current feedback (C)\n", + " 0.000305\n", " (10 / 32768)\n", " A\n", " \n", " \n", - " 9\n", - " 10\n", + " 10\n", + " 10.0\n", " Total 3 ph P\n", + " 0.137329\n", " (4500 / 32768)\n", " W\n", " \n", " \n", - " 10\n", - " 11\n", + " 11\n", + " 11.0\n", " Total 3 ph Q\n", + " 0.137329\n", " (4500 / 32768)\n", " VARs\n", " \n", " \n", - " 11\n", - " 12\n", + " 12\n", + " 12.0\n", " Total 3 ph VA\n", + " 0.137329\n", " (4500 / 32768)\n", " VA\n", " \n", " \n", - " 12\n", - " 13\n", + " 13\n", + " 13.0\n", " PF\n", + " 0.001000\n", " 0.001\n", " NaN\n", " \n", " \n", - " 13\n", - " 13\n", + " 14\n", + " 13.0\n", " Hz\n", + " 0.010000\n", " 0.01\n", " Hz\n", " \n", " \n", - " 14\n", - " 36\n", + " 15\n", + " 36.0\n", " P A\n", + " 0.137329\n", " (4500/32768)\n", " W\n", " \n", " \n", - " 15\n", - " 37\n", + " 16\n", + " 37.0\n", " P B\n", + " 0.137329\n", " (4500/32768)\n", " W\n", " \n", " \n", - " 16\n", - " 38\n", + " 17\n", + " 38.0\n", " P C\n", + " 0.137329\n", " (4500/32768)\n", " W\n", " \n", " \n", - " 17\n", - " 39\n", + " 18\n", + " 39.0\n", " Q A\n", + " 0.137329\n", " (4500/32768)\n", " VARs\n", " \n", " \n", - " 18\n", - " 40\n", + " 19\n", + " 40.0\n", " Q B\n", + " 0.137329\n", " (4500/32768)\n", " VARs\n", " \n", " \n", - " 19\n", - " 41\n", + " 20\n", + " 41.0\n", " Q C\n", + " 0.137329\n", " (4500/32768)\n", " VARs\n", " \n", @@ -187,30 +217,31 @@ "" ], "text/plain": [ - " Index Analog input: Multiplier Units\n", - "0 1 Voltage feedback A (L-N) (150 / 32768) V\n", - "1 2 Voltage feedback B (L-N) (150 / 32768) V\n", - "2 3 Voltage feedback C (L-N) (150 / 32768) V\n", - "3 4 Voltage feedback A (L-L) (300 / 32768) V\n", - "4 5 Voltage feedback B (L-L) (300 / 32768) V\n", - "5 6 Voltage feedback C (L-L) (300 / 32768) V\n", - "6 7 Current feedback (A) (10 / 32768) A\n", - "7 8 Current feedback (B) (10 / 32768) A\n", - "8 9 Current feedback (C) (10 / 32768) A\n", - "9 10 Total 3 ph P (4500 / 32768) W\n", - "10 11 Total 3 ph Q (4500 / 32768) VARs\n", - "11 12 Total 3 ph VA (4500 / 32768) VA\n", - "12 13 PF 0.001 NaN\n", - "13 13 Hz 0.01 Hz\n", - "14 36 P A (4500/32768) W\n", - "15 37 P B (4500/32768) W\n", - "16 38 P C (4500/32768) W\n", - "17 39 Q A (4500/32768) VARs\n", - "18 40 Q B (4500/32768) VARs\n", - "19 41 Q C (4500/32768) VARs" + " Index type Multiplier MultiplierString Units\n", + "0 NaN Analog input NaN NaN NaN\n", + "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V\n", + "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V\n", + "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V\n", + "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V\n", + "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V\n", + "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V\n", + "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A\n", + "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A\n", + "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A\n", + "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W\n", + "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs\n", + "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA\n", + "13 13.0 PF 0.001000 0.001 NaN\n", + "14 13.0 Hz 0.010000 0.01 Hz\n", + "15 36.0 P A 0.137329 (4500/32768) W\n", + "16 37.0 P B 0.137329 (4500/32768) W\n", + "17 38.0 P C 0.137329 (4500/32768) W\n", + "18 39.0 Q A 0.137329 (4500/32768) VARs\n", + "19 40.0 Q B 0.137329 (4500/32768) VARs\n", + "20 41.0 Q C 0.137329 (4500/32768) VARs" ] }, - "execution_count": 154, + "execution_count": 100, "metadata": {}, "output_type": "execute_result" } @@ -222,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": 155, + "execution_count": 101, "metadata": {}, "outputs": [], "source": [ @@ -232,66 +263,103 @@ }, { "cell_type": "code", - "execution_count": 156, + "execution_count": 102, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/Users/jsimpson/.conda/envs/go_solar36/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: DataFrame columns are not unique, some columns will be omitted.\n", + "/Users/jsimpson/.conda/envs/adms36/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: DataFrame columns are not unique, some columns will be omitted.\n", " \"\"\"Entry point for launching an IPython kernel.\n" ] }, { "data": { "text/plain": [ - "{1: {'Analog input:': 'Voltage feedback A (L-N)',\n", - " 'Multiplier': '(150 / 32768) ',\n", + "{nan: {'type': 'Analog input',\n", + " 'Multiplier': nan,\n", + " 'MultiplierString': nan,\n", + " 'Units': nan},\n", + " 1.0: {'type': 'Voltage feedback A (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V'},\n", - " 2: {'Analog input:': 'Voltage feedback B (L-N)',\n", - " 'Multiplier': '(150 / 32768) ',\n", + " 2.0: {'type': 'Voltage feedback B (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V'},\n", - " 3: {'Analog input:': 'Voltage feedback C (L-N)',\n", - " 'Multiplier': '(150 / 32768) ',\n", + " 3.0: {'type': 'Voltage feedback C (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V'},\n", - " 4: {'Analog input:': 'Voltage feedback A (L-L)',\n", - " 'Multiplier': '(300 / 32768) ',\n", + " 4.0: {'type': 'Voltage feedback A (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V'},\n", - " 5: {'Analog input:': 'Voltage feedback B (L-L)',\n", - " 'Multiplier': '(300 / 32768) ',\n", + " 5.0: {'type': 'Voltage feedback B (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V'},\n", - " 6: {'Analog input:': 'Voltage feedback C (L-L)',\n", - " 'Multiplier': '(300 / 32768) ',\n", + " 6.0: {'type': 'Voltage feedback C (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V'},\n", - " 7: {'Analog input:': 'Current feedback (A)',\n", - " 'Multiplier': '(10 / 32768) ',\n", + " 7.0: {'type': 'Current feedback (A)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A'},\n", - " 8: {'Analog input:': 'Current feedback (B)',\n", - " 'Multiplier': '(10 / 32768) ',\n", + " 8.0: {'type': 'Current feedback (B)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A'},\n", - " 9: {'Analog input:': 'Current feedback (C)',\n", - " 'Multiplier': '(10 / 32768) ',\n", + " 9.0: {'type': 'Current feedback (C)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A'},\n", - " 10: {'Analog input:': 'Total 3 ph P',\n", - " 'Multiplier': '(4500 / 32768) ',\n", + " 10.0: {'type': 'Total 3 ph P',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'W'},\n", - " 11: {'Analog input:': 'Total 3 ph Q',\n", - " 'Multiplier': '(4500 / 32768) ',\n", + " 11.0: {'type': 'Total 3 ph Q',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VARs'},\n", - " 12: {'Analog input:': 'Total 3 ph VA',\n", - " 'Multiplier': '(4500 / 32768) ',\n", + " 12.0: {'type': 'Total 3 ph VA',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VA'},\n", - " 13: {'Analog input:': 'Hz', 'Multiplier': 0.01, 'Units': 'Hz'},\n", - " 36: {'Analog input:': 'P A', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", - " 37: {'Analog input:': 'P B', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", - " 38: {'Analog input:': 'P C', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", - " 39: {'Analog input:': 'Q A', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", - " 40: {'Analog input:': 'Q B', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", - " 41: {'Analog input:': 'Q C', 'Multiplier': '(4500/32768)', 'Units': 'VARs'}}" + " 13.0: {'type': 'Hz',\n", + " 'Multiplier': 0.01,\n", + " 'MultiplierString': 0.01,\n", + " 'Units': 'Hz'},\n", + " 36.0: {'type': 'P A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W'},\n", + " 37.0: {'type': 'P B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W'},\n", + " 38.0: {'type': 'P C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W'},\n", + " 39.0: {'type': 'Q A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs'},\n", + " 40.0: {'type': 'Q B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs'},\n", + " 41.0: {'type': 'Q C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs'}}" ] }, - "execution_count": 156, + "execution_count": 102, "metadata": {}, "output_type": "execute_result" } @@ -304,27 +372,41 @@ }, { "cell_type": "code", - "execution_count": 157, + "execution_count": 103, "metadata": {}, "outputs": [], "source": [ "conversion_dict = {\"Shark\":shark_dict}\n", - "with open(\"conversion_dict.json\",\"w\") as f:\n", - " json.dump(conversion_dict,f,indent=2)\n" + "# with open(\"conversion_dict.json\",\"w\") as f:\n", + "# json.dump(conversion_dict,f,indent=2)\n" ] }, { "cell_type": "code", - "execution_count": 158, + "execution_count": 104, "metadata": {}, "outputs": [], "source": [ - "elements_to_device = {'632633':'Shark'}" + "# Cap example\n", + "# new Capacitor.cap1 phases=3 bus1=675.1.2.3 conn=w kv=4.16 kvar=600.00\n", + "# {\"name\":\"cap1\",\"mRID\":\"_EAC7564D-2126-4138-8525-26B63DB6FDEE\",\"CN1\":\"675\",\"phases\":\"ABC\",\"kvar_A\":200.0,\"kvar_B\":200.0,\"kvar_C\":200.0,\"nominalVoltage\":4160.0,\"nomU\":4160.0,\"phaseConnection\":\"Y\",\"grounded\":true,\"enabled\":false,\"mode\":null,\"targetValue\":0.0,\"targetDeadband\":0.0,\"aVRDelay\":0.0,\"monitoredName\":null,\"monitoredClass\":null,\"monitoredBus\":null,\"monitoredPhase\":null},\n", + "# {\"name\":\"LinearShuntCompensator_cap1\",\"mRID\":\"_0df0fb2a-6df4-4020-a545-37ca57a6570d\",\"ConductingEquipment_mRID\":\"_EAC7564D-2126-4138-8525-26B63DB6FDEE\",\"Terminal_mRID\":\"_B7CE5B58-0F81-459A-BC50-47359A840565\",\"measurementType\":\"PNV\",\"phases\":\"B\",\"MeasurementClass\":\"Analog\",\"ConductingEquipment_type\":\"LinearShuntCompensator\",\"ConductingEquipment_name\":\"cap1\",\"ConnectivityNode\":\"675\",\"SimObject\":\"cap_cap1\"},\n", + "\n", + "# Reg example\n", + "# \"regulators\":[\n", + "#{\"bankName\":\"Reg\",\"size\":\"3\",\"bankPhases\":\"ABC\",\"tankName\":[\"reg1\",\"reg2\",\"reg3\"],\"endNumber\":[2,2,2],\"endPhase\":[\"A\",\"B\",\"C\"],\"rtcName\":[\"reg1\",\"reg2\",\"reg3\"],\"mRID\":[\"_2C2316E3-C3FC-4B24-BBDE-1982F4B24138\",\"_98525341-BE02-428C-B70F-6F78850F7D49\",\"_C6C04885-5480-4C12-9FAC-46D852CEDC2B\"],\"monitoredPhase\":[\"A\",\"B\",\"C\"],\"TapChanger.tculControlMode\":[\"volt\",\"volt\",\"volt\"],\"highStep\":[16,16,16],\"lowStep\":[-16,-16,-16],\"neutralStep\":[0,0,0],\"normalStep\":[0,0,0],\"TapChanger.controlEnabled\":[true,true,true],\"lineDropCompensation\":[true,true,true],\"ltcFlag\":[true,true,true],\"RegulatingControl.enabled\":[true,true,true],\"RegulatingControl.discrete\":[true,true,true],\"RegulatingControl.mode\":[\"voltage\",\"voltage\",\"voltage\"],\"step\":[10,8,11],\"targetValue\":[122.0000,122.0000,122.0000],\"targetDeadband\":[2.0000,2.0000,2.0000],\"limitVoltage\":[0.0000,0.0000,0.0000],\"stepVoltageIncrement\":[0.6250,0.6250,0.6250],\"neutralU\":[2400.0000,2400.0000,2400.0000],\"initialDelay\":[15.0000,15.0000,15.0000],\"subsequentDelay\":[2.0000,2.0000,2.0000],\"lineDropR\":[3.0000,3.0000,3.0000],\"lineDropX\":[9.0000,9.0000,9.0000],\"reverseLineDropR\":[0.0000,0.0000,0.0000],\"reverseLineDropX\":[0.0000,0.0000,0.0000],\"ctRating\":[700.0000,700.0000,700.0000],\"ctRatio\":[3500.0000,3500.0000,3500.0000],\"ptRatio\":[20.0000,20.0000,20.0000]}\n", + "#]\n", + "## {\"name\":\"RatioTapChanger_Reg\",\"mRID\":\"_2baaea4b-5f98-4d4f-8ac3-619bb2712ce7\",\"ConductingEquipment_mRID\":\"_E682BC6D-C2E2-48D0-9D08-EBD89C2767A6\",\"Terminal_mRID\":\"_339C222B-A8C7-4900-BCFE-A91B00F059C9\",\"measurementType\":\"Pos\",\"phases\":\"A\",\"MeasurementClass\":\"Discrete\",\"ConductingEquipment_type\":\"PowerTransformer\",\"ConductingEquipment_name\":\"Reg\",\"ConnectivityNode\":\"rg60\",\"SimObject\":\"reg_Reg\"},\n", + "\n", + "element_to_device = {'632633':'Shark',\n", + " 'cap1':'Beckwith CapBank 2',\n", + " 'Reg':'Beckwith LTC'}\n", + "element_to_device = {'632633':'Shark'}" ] }, { "cell_type": "code", - "execution_count": 159, + "execution_count": 105, "metadata": {}, "outputs": [ { @@ -345,13 +427,20 @@ "from_node = '632'\n", "to_node = '633'\n", "node_name='633'\n", + "\n", "line_name = from_node+to_node\n", + "cap_name = \"cap1\"\n", + "reg_name = \"Reg\"\n", "\n", "with open(\"model_dict.json\") as f:\n", " model_dict = json.load(f)\n", "\n", - "model_line_dict={from_node+to_node:{}}\n", + "model_line_dict={line_name:{},\n", + " cap_name:{},\n", + " reg_name:{}}\n", "device_type = \"Shark\"\n", + "# device_type= \"Beckwith CapBank 2\"\n", + "# device_type = 'Beckwith LTC'\n", "if device_type == 'Shark':\n", " for meas in model_dict['feeders'][0]['measurements']:\n", " if meas['name'].startswith('ACLineSegment_'+line_name):\n", @@ -364,10 +453,48 @@ " model_line_dict[from_node+to_node]['Q '+meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", " # if meas['ConnectivityNode'] == node_name and meas['ConductingEquipment_type'] == 'ACLineSegment':\n", " # print(meas)\n", - "if device_type == 'Capacitor':\n", - " pass\n", - "if device_type == 'Regulator':\n", - " pass\n", + "elif device_type == 'Beckwith CapBank 2':\n", + " #LinearShuntCompensator\n", + " for meas in model_dict['feeders'][0]['measurements']:\n", + " if meas['name'].startswith('LinearShuntCompensator_'+cap_name):\n", + " if meas['measurementType'] == 'PNV':\n", + "# print(meas)\n", + " model_line_dict[cap_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[cap_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'angle'}\n", + " elif meas['measurementType'] == 'VA':\n", + "# print(meas)\n", + " model_line_dict[cap_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[cap_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'angle'}\n", + " elif meas['measurementType'] == 'POS':\n", + " model_line_dict[cap_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'pos'}\n", + " pass\n", + " for cap in model_dict['feeders'][0][\"capacitors\"]:\n", + " if cap['name'] == cap_name:\n", + " model_line_dict[cap_name]['manual close'] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " print(cap)\n", + " #TODO figure this out\n", + " # 0 manual close\n", + " # 1 manual open\n", + "elif device_type == 'Beckwith LTC':\n", + " for meas in model_dict['feeders'][0]['measurements']:\n", + " if meas['name'].startswith('LinearShuntCompensator_'+cap_name):\n", + " if meas['measurementType'] == 'PNV':\n", + " # print(meas)\n", + " model_line_dict[reg_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[reg_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'angle'}\n", + " elif meas['measurementType'] == 'VA':\n", + " # print(meas)\n", + " model_line_dict[reg_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[reg_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'angle'}\n", + " elif meas['measurementType'] == 'POS':\n", + " #Tap position\n", + " model_line_dict[reg_name]['? '+meas['phases']+ ' ?'] = {'mrid':meas['mRID'],'type':'pos'}\n", + " pass\n", + " for reg in model_dict['feeders'][0][\"regulators\"]:\n", + " if reg['bankName'] == reg_name:\n", + " # Do I have to count for each position change?\n", + " print(reg)\n", + " \n", "model_line_dict\n", "with open(\"model_line_dict.json\",\"w\") as f:\n", " json.dump(model_line_dict,f,indent=2)" @@ -375,7 +502,49 @@ }, { "cell_type": "code", - "execution_count": 132, + "execution_count": 106, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'632633': {'P C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'type': 'magnitude'},\n", + " 'Q C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'angle'},\n", + " 'P A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2',\n", + " 'type': 'magnitude'},\n", + " 'Q A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'Voltage feedback C (L-N)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback C (L-L)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'Voltage feedback A (L-N)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback A (L-L)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", + " 'type': 'angle'},\n", + " 'Voltage feedback B (L-N)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback B (L-L)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", + " 'type': 'angle'},\n", + " 'P B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30',\n", + " 'type': 'magnitude'},\n", + " 'Q B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + " 'cap1': {},\n", + " 'Reg': {}}" + ] + }, + "execution_count": 106, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_line_dict" + ] + }, + { + "cell_type": "code", + "execution_count": 107, "metadata": {}, "outputs": [], "source": [ @@ -389,145 +558,610 @@ }, { "cell_type": "code", - "execution_count": 147, + "execution_count": 108, + "metadata": {}, + "outputs": [], + "source": [ + "# index = 1\n", + "# values = {1:28008,2:0,3:0,4:0.11,5:0,6:0,36:200,37:0,38:0,39:0.12,40:0,41:0}\n", + "# cim_msg = {}\n", + "\n", + "# for name, device in element_to_device.items():\n", + "# model = model_line_dict[name]\n", + "# conversion = conversion_dict[device]\n", + "\n", + "# for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", + "# value = values[index]\n", + "# dnp3_attr_name = conversion['Analog input'][index]['type']\n", + "# # print(meas_type,model[meas_type])\n", + "# if model[dnp3_attr_name]['mrid'] not in cim_msg:\n", + "# cim_msg[model[dnp3_attr_name]['mrid']] = {'magnitude':0,'angle':0}\n", + "# value_type = model[dnp3_attr_name]['type']\n", + "# cim_msg[model[dnp3_attr_name]['mrid']][value_type] = value # time multipier\n", + "# # for type_name, mrid in model.items():\n", + "# # print(conversion[index],type_name)\n", + "# cim_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 109, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'_806d5de7-541b-4944-8825-c7afffa5abc2': {'magnitude': 28008, 'angle': 0.11},\n", - " '_bd5f3945-5d16-4694-896e-92da8a0e4895': {'magnitude': 0, 'angle': 0},\n", - " '_5bc367a5-f771-494a-83bd-b4560d538df7': {'magnitude': 0, 'angle': 0},\n", - " '_4044e33a-ecc2-4d3d-8584-f65643c4eea2': {'magnitude': 200, 'angle': 0.12},\n", - " '_ebd287b8-bceb-4471-a709-c82fbd34bd30': {'magnitude': 0, 'angle': 0},\n", - " '_11bf885c-b677-42ba-859c-d1a013a9f36e': {'magnitude': 0, 'angle': 0}}" + "{'P C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'magnitude'},\n", + " 'Q C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'angle'},\n", + " 'P A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'magnitude'},\n", + " 'Q A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'Voltage feedback C (L-N)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback C (L-L)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'Voltage feedback A (L-N)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback A (L-L)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", + " 'type': 'angle'},\n", + " 'Voltage feedback B (L-N)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", + " 'type': 'magnitude'},\n", + " 'Voltage feedback B (L-L)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", + " 'type': 'angle'},\n", + " 'P B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'magnitude'},\n", + " 'Q B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}}" ] }, - "execution_count": 147, + "execution_count": 109, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "index = 1\n", - "values = {1:28008,2:0,3:0,4:0.11,5:0,6:0,36:200,37:0,38:0,39:0.12,40:0,41:0}\n", - "cim_msg = {}\n", + "model" + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n" + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'exclusion_dict' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexclusion_dict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_csv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"exclusion.csv\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'exclusion_dict' is not defined" + ] + } + ], + "source": [ + "pd.DataFrame.from_dict(exclusion_dict).to_csv(\"exclusion.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "metadata": {}, + "outputs": [], + "source": [ + "def get_conversion_model(csv_file,sheet_name):\n", + " df = pd.read_excel(csv_file,sheet_name=sheet_name)\n", + " master_dict ={\n", + " 'Analog input':{},\n", + " 'Analog output':{},\n", + " 'Binary input':{},\n", + " 'Binary output':{} }\n", "\n", - "for name, device in element_to_device.items():\n", - " model = model_line_dict[name]\n", - " conversion = conversion_dict[device]\n", + "# temp_df = pd.DataFrame(df[1:4])\n", + "# temp_df = temp_df.set_index('Index')\n", + "# master_dict['Analog input'] = temp_df.T\n", "\n", - " for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", - " value = values[index]\n", - " meas_type = conversion[str(index)]['Analog input:']\n", - "# print(meas_type,model[meas_type])\n", - " if model[meas_type]['mrid'] not in cim_msg:\n", - " cim_msg[model[meas_type]['mrid']] = {'magnitude':0,'angle':0}\n", - " value_type = model[meas_type]['type']\n", - " cim_msg[model[meas_type]['mrid']][value_type] = value\n", - " # for type_name, mrid in model.items():\n", - " # print(conversion[index],type_name)\n", - "cim_msg" + "# temp_df = pd.DataFrame(df[6:10])\n", + "# temp_df = temp_df.set_index('Index')\n", + "# master_dict['Analog output'] = temp_df.T\n", + "\n", + " x = []\n", + "\n", + " for row in df.iterrows():\n", + " if pd.isna(row[1][2]):\n", + " # print(row[0])\n", + " x.append(row[0])\n", + "# print(df.shape)\n", + " x.append(df.shape[0])\n", + " it = iter(x)\n", + " for x in it:\n", + " type_name = df.iloc[x][1]\n", + " print(type_name)\n", + " next_value = next(it)\n", + " print (x+1, next_value)\n", + " print(pd.DataFrame(df[x+1:next_value]))\n", + " temp_df = pd.DataFrame(df[x+1: next_value])\n", + " temp_df = temp_df.set_index('Index')\n", + " master_dict[type_name] = temp_df.T.to_dict()\n", + " return master_dict\n" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "metadata": {}, + "outputs": [], + "source": [ + "csv_file = 'DNP3 list.xlsx'" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analog input\n", + "1 21\n", + " Index type Multiplier MultiplierString Units\n", + "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V\n", + "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V\n", + "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V\n", + "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V\n", + "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V\n", + "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V\n", + "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A\n", + "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A\n", + "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A\n", + "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W\n", + "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs\n", + "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA\n", + "13 13.0 PF 0.001000 0.001 NaN\n", + "14 13.0 Hz 0.010000 0.01 Hz\n", + "15 36.0 P A 0.137329 (4500/32768) W\n", + "16 37.0 P B 0.137329 (4500/32768) W\n", + "17 38.0 P C 0.137329 (4500/32768) W\n", + "18 39.0 Q A 0.137329 (4500/32768) VARs\n", + "19 40.0 Q B 0.137329 (4500/32768) VARs\n", + "20 41.0 Q C 0.137329 (4500/32768) VARs\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jsimpson/.conda/envs/adms36/lib/python3.6/site-packages/ipykernel_launcher.py:34: UserWarning: DataFrame columns are not unique, some columns will be omitted.\n" + ] + } + ], + "source": [ + "shark = get_conversion_model(csv_file, sheet_name='Shark')\n", + "conversion_dict['Shark'] = shark" ] }, { "cell_type": "code", - "execution_count": 139, + "execution_count": 115, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'1': {'Analog input:': 'Voltage feedback A (L-N)',\n", - " 'Multiplier': '(150 / 32768) ',\n", + "{1.0: {'type': 'Voltage feedback A (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V'},\n", - " '2': {'Analog input:': 'Voltage feedback B (L-N)',\n", - " 'Multiplier': '(150 / 32768) ',\n", + " 2.0: {'type': 'Voltage feedback B (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V'},\n", - " '3': {'Analog input:': 'Voltage feedback C (L-N)',\n", - " 'Multiplier': '(150 / 32768) ',\n", + " 3.0: {'type': 'Voltage feedback C (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V'},\n", - " '4': {'Analog input:': 'Voltage feedback A (L-L)',\n", - " 'Multiplier': '(300 / 32768) ',\n", + " 4.0: {'type': 'Voltage feedback A (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V'},\n", - " '5': {'Analog input:': 'Voltage feedback B (L-L)',\n", - " 'Multiplier': '(300 / 32768) ',\n", + " 5.0: {'type': 'Voltage feedback B (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V'},\n", - " '6': {'Analog input:': 'Voltage feedback C (L-L)',\n", - " 'Multiplier': '(300 / 32768) ',\n", + " 6.0: {'type': 'Voltage feedback C (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V'},\n", - " '7': {'Analog input:': 'Current feedback\\xa0(A)',\n", - " 'Multiplier': '(10 / 32768) ',\n", + " 7.0: {'type': 'Current feedback (A)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A'},\n", - " '8': {'Analog input:': 'Current feedback\\xa0(B)',\n", - " 'Multiplier': '(10 / 32768) ',\n", + " 8.0: {'type': 'Current feedback (B)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A'},\n", - " '9': {'Analog input:': 'Current feedback\\xa0(C)',\n", - " 'Multiplier': '(10 / 32768) ',\n", + " 9.0: {'type': 'Current feedback (C)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A'},\n", - " '10': {'Analog input:': 'Total 3 ph P',\n", - " 'Multiplier': '(4500 / 32768) ',\n", + " 10.0: {'type': 'Total 3 ph P',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'W'},\n", - " '11': {'Analog input:': 'Total 3 ph Q',\n", - " 'Multiplier': '(4500 / 32768) ',\n", + " 11.0: {'type': 'Total 3 ph Q',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VARs'},\n", - " '12': {'Analog input:': 'Total 3 ph VA',\n", - " 'Multiplier': '(4500 / 32768) ',\n", + " 12.0: {'type': 'Total 3 ph VA',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VA'},\n", - " '13': {'Analog input:': 'Hz', 'Multiplier': 0.01, 'Units': 'Hz'},\n", - " '36': {'Analog input:': 'P A', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", - " '37': {'Analog input:': 'P B', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", - " '38': {'Analog input:': 'P C', 'Multiplier': '(4500/32768)', 'Units': 'W'},\n", - " '39': {'Analog input:': 'Q A', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", - " '40': {'Analog input:': 'Q B', 'Multiplier': '(4500/32768)', 'Units': 'VARs'},\n", - " '41': {'Analog input:': 'Q C', 'Multiplier': '(4500/32768)', 'Units': 'VARs'}}" + " 13.0: {'type': 'Hz',\n", + " 'Multiplier': 0.01,\n", + " 'MultiplierString': 0.01,\n", + " 'Units': 'Hz'},\n", + " 36.0: {'type': 'P A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W'},\n", + " 37.0: {'type': 'P B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W'},\n", + " 38.0: {'type': 'P C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W'},\n", + " 39.0: {'type': 'Q A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs'},\n", + " 40.0: {'type': 'Q B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs'},\n", + " 41.0: {'type': 'Q C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs'}}" ] }, - "execution_count": 139, + "execution_count": 115, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "conversion = conversion_dict[device]\n", - "conversion" + "shark['Analog input']" ] }, { "cell_type": "code", - "execution_count": 101, + "execution_count": 116, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analog input\n", + "1 4\n", + " Index Type Multiplier Units Group Variation\n", + "1 0.0 Voltage feedback (secondary) 0.1 V NaN NaN\n", + "2 11111.0 Current feedback (Secondary) 1.0 V NaN NaN\n", + "3 3.0 Primary VArs 1.0 NaN NaN NaN\n", + "Analog output\n", + "6 10\n", + " Index Type Multiplier Units Group Variation\n", + "6 7.0 Overvoltage Limit 1.0 NaN NaN NaN\n", + "7 8.0 Undervoltage limit 1.0 NaN NaN NaN\n", + "8 11111.0 Close delay time 1.0 NaN NaN NaN\n", + "9 11112.0 open delay time 1.0 NaN NaN NaN\n", + "Binary output\n", + "12 15\n", + " Index Type Multiplier Units Group Variation\n", + "12 0.0 manual close 1.0 Binary NaN NaN\n", + "13 1.0 manual open 1.0 Binary NaN NaN\n", + "14 11111.0 Scada Control 1.0 Binary NaN NaN\n", + "Binary input\n", + "17 24\n", + " Index Type Multiplier Units Group Variation\n", + "17 11111.0 Enabled 1.0 Binary NaN NaN\n", + "18 6.0 Remote 1.0 Binary NaN NaN\n", + "19 7.0 Local 1.0 Binary NaN NaN\n", + "20 4.0 Manual 1.0 Binary NaN NaN\n", + "21 5.0 Auto 1.0 Binary NaN NaN\n", + "22 0.0 Close status 1.0 Binary NaN NaN\n", + "23 1.0 Open Status 1.0 Binary NaN NaN\n", + "Analog input\n", + "1 4\n", + " Index Type Multiplier Units Group \\\n", + "1 0.0 Load Voltage feedback (secondary) 1.0 NaN NaN \n", + "2 2.0 Load Current feedback (Secondary) 1.0 NaN NaN \n", + "3 5.0 Tap position 1.0 NaN NaN \n", + "\n", + " Variation \n", + "1 NaN \n", + "2 NaN \n", + "3 NaN \n", + "Analog output:\n", + "6 13\n", + " Index Type Multiplier Units Group \\\n", + "6 0.0 Fail safe Voltage set point 1.0 NaN NaN \n", + "7 2.0 Fail Safe Voltage bandwidth (dead band) 1.0 NaN NaN \n", + "8 34.0 Emergency Voltage set point 1.0 NaN NaN \n", + "9 236.0 Emergency Voltage bandwidth  (dead band) 1.0 NaN NaN \n", + "10 15.0 Voltage max 1.0 NaN NaN \n", + "11 14.0 voltage min 1.0 NaN NaN \n", + "12 4.0 Time Delay 1.0 NaN NaN \n", + "\n", + " Variation \n", + "6 NaN \n", + "7 NaN \n", + "8 NaN \n", + "9 NaN \n", + "10 NaN \n", + "11 NaN \n", + "12 NaN \n", + "Binary output:\n", + "15 19\n", + " Index Type Multiplier Units Group Variation\n", + "15 0.0 manual raise 1 Tap 1.0 NaN NaN NaN\n", + "16 1.0 manual Lower 1 Tap 1.0 NaN NaN NaN\n", + "17 5.0 Scada Control 1.0 NaN NaN NaN\n", + "18 277.0 Enabled 1.0 NaN NaN NaN\n", + "Binary input:\n", + "21 24\n", + " Index Type Multiplier Units Group Variation\n", + "21 11.0 In band 1.0 NaN NaN NaN\n", + "22 27.0 Remote/Local 1.0 NaN NaN NaN\n", + "23 37.0 Auto/Manual 1.0 NaN NaN NaN\n" + ] + } + ], + "source": [ + "sheet_name='Beckwith CapBank 2'\n", + "beckwith_capbank = get_conversion_model(csv_file,sheet_name)\n", + "conversion_dict[sheet_name]= beckwith_capbank\n", + "sheet_name ='Beckwith LTC'\n", + "beckwith_capbank = get_conversion_model(csv_file,sheet_name)\n", + "conversion_dict[sheet_name]= beckwith_capbank\n", + "with open(\"conversion_dict.json\",\"w\") as f:\n", + " json.dump(conversion_dict,f,indent=2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 117, "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
typeMultiplierMultiplierStringUnits
Index
1.0Voltage feedback A (L-N)0.004578(150 / 32768)V
2.0Voltage feedback B (L-N)0.004578(150 / 32768)V
3.0Voltage feedback C (L-N)0.004578(150 / 32768)V
4.0Voltage feedback A (L-L)0.009155(300 / 32768)V
\n", + "
" + ], "text/plain": [ - "{'P C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'magnitude'},\n", - " 'Q C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'angle'},\n", - " 'P A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'magnitude'},\n", - " 'Q A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", - " 'Voltage feedback C (L-N)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback C (L-L)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", - " 'type': 'angle'},\n", - " 'Voltage feedback A (L-N)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback A (L-L)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", - " 'type': 'angle'},\n", - " 'Voltage feedback B (L-N)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback B (L-L)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", - " 'type': 'angle'},\n", - " 'P B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'magnitude'},\n", - " 'Q B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}}" + " type Multiplier MultiplierString Units\n", + "Index \n", + "1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V\n", + "2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V\n", + "3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V\n", + "4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V" ] }, - "execution_count": 101, + "execution_count": 117, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "model" + "pd.DataFrame(df[1:4])" + ] + }, + { + "cell_type": "code", + "execution_count": 118, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{0.0: {'Type': 'Load Voltage feedback (secondary)',\n", + " 'Multiplier': 1.0,\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan},\n", + " 2.0: {'Type': 'Load Current feedback\\xa0(Secondary)',\n", + " 'Multiplier': 1.0,\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan},\n", + " 5.0: {'Type': 'Tap position',\n", + " 'Multiplier': 1.0,\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan}}" + ] + }, + "execution_count": 118, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beckwith_capbank['Analog input']" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Type': 'Load Voltage feedback (secondary)',\n", + " 'Multiplier': 1.0,\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan}" + ] + }, + "execution_count": 119, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beckwith_capbank['Analog input'][0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 120, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_806d5de7-541b-4944-8825-c7afffa5abc2': {'magnitude': 128.21044897368,\n", + " 'angle': 0.0010070800773},\n", + " '_bd5f3945-5d16-4694-896e-92da8a0e4895': {'magnitude': 0.0, 'angle': 0.0},\n", + " '_5bc367a5-f771-494a-83bd-b4560d538df7': {'magnitude': 0.0, 'angle': 0.0},\n", + " '_4044e33a-ecc2-4d3d-8584-f65643c4eea2': {'magnitude': 27.465820311999998,\n", + " 'angle': 0.0164794921872},\n", + " '_ebd287b8-bceb-4471-a709-c82fbd34bd30': {'magnitude': 0.0, 'angle': 0.0},\n", + " '_11bf885c-b677-42ba-859c-d1a013a9f36e': {'magnitude': 0.0, 'angle': 0.0}}" + ] + }, + "execution_count": 120, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "index = 1\n", + "values = {1:28008,2:0,3:0,4:0.11,5:0,6:0,36:200,37:0,38:0,39:0.12,40:0,41:0}\n", + "cim_msg = {}\n", + "\n", + "for name, device in element_to_device.items():\n", + " model = model_line_dict[name]\n", + " conversion = conversion_dict[device]\n", + "\n", + " for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", + " value = values[index]\n", + " dnp3_attr_name = conversion['Analog input'][index]['type']\n", + " ## Check if multiplier is na or str\n", + " multiplier = conversion['Analog input'][index]['Multiplier']\n", + " if type(multiplier) == str:\n", + " multiplier = 1 \n", + " \n", + "# print(meas_type,model[meas_type])\n", + " if model[dnp3_attr_name]['mrid'] not in cim_msg:\n", + " cim_msg[model[dnp3_attr_name]['mrid']] = {'magnitude':0,'angle':0}\n", + " value_type = model[dnp3_attr_name]['type']\n", + " cim_msg[model[dnp3_attr_name]['mrid']][value_type] = value * multiplier # times multipier\n", + " # for type_name, mrid in model.items():\n", + " # print(conversion[index],type_name)\n", + "cim_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.13732910156" + ] + }, + "execution_count": 121, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "conversion['Analog input'][index]['Multiplier']" ] }, { @@ -554,7 +1188,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.6.10" } }, "nbformat": 4, diff --git a/test/build_conversion_dict.py b/test/build_conversion_dict.py new file mode 100644 index 0000000..8049b4d --- /dev/null +++ b/test/build_conversion_dict.py @@ -0,0 +1,11 @@ +import sys +sys.path.append("../dnp3/service") + +from dnp3.dnp3_to_cim import build_conversion + +''' +Build the conversion dictionary from the DNP3 list.xlsx +''' +if __name__ == '__main__': + csv_file = "DNP3 list.xlsx" + build_conversion(csv_file=csv_file) \ No newline at end of file diff --git a/test/fim_receiver.py b/test/fim_receiver.py new file mode 100644 index 0000000..1321c73 --- /dev/null +++ b/test/fim_receiver.py @@ -0,0 +1,28 @@ + +from gridappsd import GridAPPSD, DifferenceBuilder, utils +from time import sleep + +def on_message(simulation_id,message): + print("Message received:", simulation_id['message-id']) + print(message) + # if(dnp3_object_list.__len__() > 0): + # updates = dnp3_object_list[0].create_message_updates(simulation_id, message) + # if updates is None: + # print("NONE Jeff") + # return + # print("Outstation Updates Created") + # + # for cimMapping in dnp3_object_list: + # cimMapping.outstation.apply_compiled_updates(updates) + + # print("Done updating outstations") + +if __name__ == '__main__': + + simulation_id = str(1234) + gapps = GridAPPSD(simulation_id, address=utils.get_gridappsd_address(), + username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + + gapps.subscribe('/topic/goss.gridappsd.fim.input.' + simulation_id, on_message) + while True: + sleep(0.01) \ No newline at end of file diff --git a/test/master_cmd.py b/test/master_cmd.py index f05ccb5..f2405a3 100644 --- a/test/master_cmd.py +++ b/test/master_cmd.py @@ -2,7 +2,7 @@ import logging import sys -sys.path.append("../service") +sys.path.append("../dnp3/service") from datetime import datetime from pydnp3 import opendnp3, openpal diff --git a/test/master_main.py b/test/master_main.py index f79c5be..339bd1b 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -1,27 +1,37 @@ import os import sys +import time sys.path.append("../dnp3/service") from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication from dnp3.dnp3_to_cim import CIMMapping from pydnp3 import opendnp3, openpal -def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): +# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): +def run_master(device_ip_port_config_all, names): + masters = [] dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json") - elements_to_device = {'632633': 'Shark'} - application_1 = MyMaster(HOST=HOST, # "127.0.0.1 - LOCAL="0.0.0.0", - PORT=int(PORT), - DNP3_ADDR=int(DNP3_ADDR), - log_handler=MyLogger(), - listener=AppChannelListener(), - soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), - master_application=MasterApplication()) + for name in names: + device_ip_port_dict = device_ip_port_config_all[name] + HOST=device_ip_port_dict['ip'] + PORT=device_ip_port_dict['port'] + DNP3_ADDR= device_ip_port_dict['link_local_addr'] + convertion_type=device_ip_port_dict['conversion_type'] + object_name='632633' + + elements_to_device = {'632633': 'Shark'} + application_1 = MyMaster(HOST=HOST, # "127.0.0.1 + LOCAL="0.0.0.0", + PORT=int(PORT), + DNP3_ADDR=int(DNP3_ADDR), + log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), + master_application=MasterApplication()) + # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) + masters.append(application_1) - masters = [application_1] - # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) - # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) - import time SLEEP_SECONDS = 1 time.sleep(SLEEP_SECONDS) # group_variation = opendnp3.GroupVariationID(32, 2) @@ -37,15 +47,17 @@ def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark # application_1.fast_scan_all.Demand() - for master in masters: - master.fast_scan_all.Demand() + # for master in masters: + # master.fast_scan_all.Demand() while True: cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} for master in masters: - cim_msg = master.soe_handler._cim_msg + cim_msg = master.soe_handler.get_msg() + # print(cim_msg) cim_full_msg['messages'].update(cim_msg) + print(cim_full_msg) - time.sleep(60) + time.sleep(10) print('\nStopping') @@ -68,16 +80,19 @@ def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark import argparse import json parser = argparse.ArgumentParser() - parser.add_argument("name", help="name of dnp3 outstation") + parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') args = parser.parse_args() + print(args.names) + # exit(0) with open("device_ip_port_config_all.json") as f: device_ip_port_config_all = json.load(f) - device_ip_port_dict = device_ip_port_config_all[args.name] + device_ip_port_dict = device_ip_port_config_all[args.names[0]] print(device_ip_port_dict) - run_master(device_ip_port_dict['ip'], - device_ip_port_dict['port'], - device_ip_port_dict['link_local_addr'], - device_ip_port_dict['conversion_type'], - '632633') \ No newline at end of file + run_master(device_ip_port_config_all, args.names) + # run_master(device_ip_port_dict['ip'], + # device_ip_port_dict['port'], + # device_ip_port_dict['link_local_addr'], + # device_ip_port_dict['conversion_type'], + # '632633') \ No newline at end of file diff --git a/test/outstation.py b/test/outstation.py new file mode 100644 index 0000000..cd4f1fd --- /dev/null +++ b/test/outstation.py @@ -0,0 +1,303 @@ +import logging +import sys + +from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 + +LOG_LEVELS = opendnp3.levels.NORMAL | opendnp3.levels.ALL_COMMS +# LOCAL_IP = "0.0.0.0" +# PORT = 20000 + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) + + +class OutstationApplication(opendnp3.IOutstationApplication): + """ + Interface for all outstation callback info except for control requests. + + DNP3 spec section 5.1.6.2: + The Application Layer provides the following services for the DNP3 User Layer in an outstation: + - Notifies the DNP3 User Layer when action requests, such as control output, + analog output, freeze and file operations, arrive from a master. + - Requests data and information from the outstation that is wanted by a master + and formats the responses returned to a master. + - Assures that event data is successfully conveyed to a master (using + Application Layer confirmation). + - Sends notifications to the master when the outstation restarts, has queued events, + and requires time synchronization. + + DNP3 spec section 5.1.6.3: + The Application Layer requires specific services from the layers beneath it. + - Partitioning of fragments into smaller portions for transport reliability. + - Knowledge of which device(s) were the source of received messages. + - Transmission of messages to specific devices or to all devices. + - Message integrity (i.e., error-free reception and transmission of messages). + - Knowledge of the time when messages arrive. + - Either precise times of transmission or the ability to set time values + into outgoing messages. + """ + + outstation = None + + def __init__(self, + LOCAL_IP="0.0.0.0", + PORT=20000, + DNP3_ADDR=1, config={}): + self.DNP3_ADDR = DNP3_ADDR + self.config = config + super(OutstationApplication, self).__init__() + + _log.debug('Configuring the DNP3 stack.') + self.stack_config = self.configure_stack() + self.stack_config.link.LocalAddr = self.DNP3_ADDR ## Was 10 + # self.stack_config.link.RemoteAddr = 1 ## TODO + + _log.debug('Configuring the outstation database.') + self.configure_database(self.stack_config.dbConfig) + + _log.debug('Creating a DNP3Manager.') + threads_to_allocate = 1 + # self.log_handler = MyLogger() + self.log_handler = asiodnp3.ConsoleLogger().Create() # (or use this during regression testing) + self.manager = asiodnp3.DNP3Manager(threads_to_allocate, self.log_handler) + + _log.debug('Creating the DNP3 channel, a TCP server.') + self.retry_parameters = asiopal.ChannelRetry().Default() + self.listener = AppChannelListener() + # self.listener = asiodnp3.PrintingChannelListener().Create() # (or use this during regression testing) + self.channel = self.manager.AddTCPServer("server", + LOG_LEVELS, + self.retry_parameters, + LOCAL_IP, + PORT, + self.listener) + + _log.debug('Adding the outstation to the channel.') + self.command_handler = OutstationCommandHandler() + # self.command_handler = opendnp3.SuccessCommandHandler().Create() # (or use this during regression testing) + self.outstation = self.channel.AddOutstation("outstation", self.command_handler, self, self.stack_config) + + # Put the Outstation singleton in OutstationApplication so that it can be used to send updates to the Master. + OutstationApplication.set_outstation(self.outstation) + + _log.debug('Enabling the outstation. Traffic will now start to flow.') + self.outstation.Enable() + + @staticmethod + def configure_stack(): + """Set up the OpenDNP3 configuration.""" + stack_config = asiodnp3.OutstationStackConfig(opendnp3.DatabaseSizes.AllTypes(10)) + stack_config.outstation.eventBufferConfig = opendnp3.EventBufferConfig().AllTypes(10) + stack_config.outstation.params.allowUnsolicited = True + stack_config.link.LocalAddr = 1024 ## Was 10 + stack_config.link.LocalAddr = 10 ## Was 10 + stack_config.link.RemoteAddr = 1 ## TODO Fix + stack_config.link.KeepAliveTimeout = openpal.TimeDuration().Max() + return stack_config + + @staticmethod + def configure_database(db_config): + """ + Configure the Outstation's database of input point definitions. + + Configure two Analog points (group/variation 30.1) at indexes 1 and 2. + Configure two Binary points (group/variation 1.2) at indexes 1 and 2. + """ + db_config.analog[1].clazz = opendnp3.PointClass.Class2 + db_config.analog[1].svariation = opendnp3.StaticAnalogVariation.Group30Var1 + db_config.analog[1].evariation = opendnp3.EventAnalogVariation.Group32Var7 + db_config.analog[2].clazz = opendnp3.PointClass.Class2 + db_config.analog[2].svariation = opendnp3.StaticAnalogVariation.Group30Var1 + db_config.analog[2].evariation = opendnp3.EventAnalogVariation.Group32Var7 + db_config.binary[1].clazz = opendnp3.PointClass.Class2 + db_config.binary[1].svariation = opendnp3.StaticBinaryVariation.Group1Var2 + db_config.binary[1].evariation = opendnp3.EventBinaryVariation.Group2Var2 + db_config.binary[2].clazz = opendnp3.PointClass.Class2 + db_config.binary[2].svariation = opendnp3.StaticBinaryVariation.Group1Var2 + db_config.binary[2].evariation = opendnp3.EventBinaryVariation.Group2Var2 + + def shutdown(self): + """ + Execute an orderly shutdown of the Outstation. + + The debug messages may be helpful if errors occur during shutdown. + """ + # _log.debug('Exiting application...') + # _log.debug('Shutting down outstation...') + # OutstationApplication.set_outstation(None) + # _log.debug('Shutting down stack config...') + # self.stack_config = None + # _log.debug('Shutting down channel...') + # self.channel = None + # _log.debug('Shutting down DNP3Manager...') + # self.manager = None + + self.manager.Shutdown() + + @classmethod + def get_outstation(cls): + """Get the singleton instance of IOutstation.""" + return cls.outstation + + @classmethod + def set_outstation(cls, outstn): + """ + Set the singleton instance of IOutstation, as returned from the channel's AddOutstation call. + + Making IOutstation available as a singleton allows other classes (e.g. the command-line UI) + to send commands to it -- see apply_update(). + """ + cls.outstation = outstn + + # Overridden method + def ColdRestartSupport(self): + """Return a RestartMode enumerated value indicating whether cold restart is supported.""" + _log.debug('In OutstationApplication.ColdRestartSupport') + return opendnp3.RestartMode.UNSUPPORTED + + # Overridden method + def GetApplicationIIN(self): + """Return the application-controlled IIN field.""" + application_iin = opendnp3.ApplicationIIN() + application_iin.configCorrupt = False + application_iin.deviceTrouble = False + application_iin.localControl = False + application_iin.needTime = False + # Just for testing purposes, convert it to an IINField and display the contents of the two bytes. + iin_field = application_iin.ToIIN() + _log.debug('OutstationApplication.GetApplicationIIN: IINField LSB={}, MSB={}'.format(iin_field.LSB, + iin_field.MSB)) + return application_iin + + # Overridden method + def SupportsAssignClass(self): + _log.debug('In OutstationApplication.SupportsAssignClass') + return False + + # Overridden method + def SupportsWriteAbsoluteTime(self): + _log.debug('In OutstationApplication.SupportsWriteAbsoluteTime') + return False + + # Overridden method + def SupportsWriteTimeAndInterval(self): + _log.debug('In OutstationApplication.SupportsWriteTimeAndInterval') + return False + + # Overridden method + def WarmRestartSupport(self): + """Return a RestartMode enumerated value indicating whether a warm restart is supported.""" + _log.debug('In OutstationApplication.WarmRestartSupport') + return opendnp3.RestartMode.UNSUPPORTED + + @classmethod + def process_point_value(cls, command_type, command, index, op_type): + """ + A PointValue was received from the Master. Process its payload. + + :param command_type: (string) Either 'Select' or 'Operate'. + :param command: A ControlRelayOutputBlock or else a wrapped data value (AnalogOutputInt16, etc.). + :param index: (integer) DNP3 index of the payload's data definition. + :param op_type: An OperateType, or None if command_type == 'Select'. + """ + _log.debug('Processing received point value for index {}: {}'.format(index, command)) + + def apply_update(self, value, index): + """ + Record an opendnp3 data value (Analog, Binary, etc.) in the outstation's database. + + The data value gets sent to the Master as a side-effect. + + :param value: An instance of Analog, Binary, or another opendnp3 data value. + :param index: (integer) Index of the data definition in the opendnp3 database. + """ + _log.debug('Recording {} measurement, index={}, value={}'.format(type(value).__name__, index, value.value)) + builder = asiodnp3.UpdateBuilder() + builder.Update(value, index) + update = builder.Build() + OutstationApplication.get_outstation().Apply(update) + + +class OutstationCommandHandler(opendnp3.ICommandHandler): + """ + Override ICommandHandler in this manner to implement application-specific command handling. + + ICommandHandler implements the Outstation's handling of Select and Operate, + which relay commands and data from the Master to the Outstation. + """ + + def Start(self): + _log.debug('In OutstationCommandHandler.Start') + + def End(self): + _log.debug('In OutstationCommandHandler.End') + + def Select(self, command, index): + """ + The Master sent a Select command to the Outstation. Handle it. + + :param command: ControlRelayOutputBlock, + AnalogOutputInt16, AnalogOutputInt32, AnalogOutputFloat32, or AnalogOutputDouble64. + :param index: int + :return: CommandStatus + """ + OutstationApplication.process_point_value('Select', command, index, None) + return opendnp3.CommandStatus.SUCCESS + + def Operate(self, command, index, op_type): + """ + The Master sent an Operate command to the Outstation. Handle it. + + :param command: ControlRelayOutputBlock, + AnalogOutputInt16, AnalogOutputInt32, AnalogOutputFloat32, or AnalogOutputDouble64. + :param index: int + :param op_type: OperateType + :return: CommandStatus + """ + OutstationApplication.process_point_value('Operate', command, index, op_type) + return opendnp3.CommandStatus.SUCCESS + + +class AppChannelListener(asiodnp3.IChannelListener): + """ + Override IChannelListener in this manner to implement application-specific channel behavior. + """ + + def __init__(self): + super(AppChannelListener, self).__init__() + + def OnStateChange(self, state): + _log.debug('In AppChannelListener.OnStateChange: state={}'.format(state)) + + +class MyLogger(openpal.ILogHandler): + """ + Override ILogHandler in this manner to implement application-specific logging behavior. + """ + + def __init__(self): + super(MyLogger, self).__init__() + + def Log(self, entry): + filters = entry.filters.GetBitfield() + location = entry.location.rsplit('/')[-1] if entry.location else '' + message = entry.message + _log.debug('Log\tfilters={}\tlocation={}\tentry={}'.format(filters, location, message)) + + +def main(): + """The Outstation has been started from the command line. Execute ad-hoc tests if desired.""" + app = OutstationApplication() + _log.debug('Initialization complete. In command loop.') + # Ad-hoc tests can be inserted here if desired. See outstation_cmd.py for examples. + app.shutdown() + _log.debug('Exiting.') + exit() + + +if __name__ == '__main__': + main() diff --git a/test/outstation_main.py b/test/outstation_main.py new file mode 100644 index 0000000..b18b907 --- /dev/null +++ b/test/outstation_main.py @@ -0,0 +1,79 @@ +import os +import sys +import time + +from pydnp3 import opendnp3 + +sys.path.append("../dnp3/service") +from dnp3.dnp3_to_cim import CIMMapping + +from outstation import OutstationApplication + +# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): +def run_outstation(device_ip_port_config_all, names): + masters = [] + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json") + for name in names: + device_ip_port_dict = device_ip_port_config_all[name] + HOST=device_ip_port_dict['ip'] + PORT=device_ip_port_dict['port'] + DNP3_ADDR= device_ip_port_dict['link_local_addr'] + convertion_type=device_ip_port_dict['conversion_type'] + object_name='632633' + + elements_to_device = {'632633': 'Shark'} + application_1 = OutstationApplication( + LOCAL_IP="0.0.0.0", + PORT=int(PORT), + DNP3_ADDR=int(DNP3_ADDR), config=device_ip_port_dict) + + # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) + masters.append(application_1) + + while True: + # cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} + for master in masters: + master.config["conversion_type"] + for index, value in dnp3_to_cim.conversion_dict[master.config["conversion_type"]]['Analog input'].items(): + print(index) + int(float(index)) + master.apply_update(opendnp3.Analog(1000.3 + float(index)), int(float(index))) + time.sleep(10) + + print('\nStopping') + for master in masters: + master.shutdown() + # application_1.shutdown() + exit() + + # When terminating, it is necessary to set these to None so that + # it releases the shared pointer. Otherwise, python will not + # terminate (and even worse, the normal Ctrl+C won't help). + application_1.master.Disable() + application_1 = None + application_1.channel.Shutdown() + application_1.channel = None + application_1.manager.Shutdown() + +if __name__ == "__main__": + + import argparse + import json + parser = argparse.ArgumentParser() + parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) + # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') + args = parser.parse_args() + print(args.names) + # exit(0) + with open("device_ip_port_config_all.json") as f: + device_ip_port_config_all = json.load(f) + + device_ip_port_dict = device_ip_port_config_all[args.names[0]] + print(device_ip_port_dict) + run_outstation(device_ip_port_config_all, args.names) + # run_master(device_ip_port_dict['ip'], + # device_ip_port_dict['port'], + # device_ip_port_dict['link_local_addr'], + # device_ip_port_dict['conversion_type'], + # '632633') \ No newline at end of file diff --git a/test/ports.ipynb b/test/ports.ipynb new file mode 100644 index 0000000..7552676 --- /dev/null +++ b/test/ports.ipynb @@ -0,0 +1,108 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# ports.json\n", + "device_ip_port_config_all = {\n", + " \"test outstation 1\":{\"name\":\"test outstation 1\",\n", + " \"conversion_type\":\"Shark\",\n", + " \"CIM object\":\"632633\",\n", + " \"port\": \"20000\",\n", + " \"ip\": \"127.0.0.1\",\n", + " \"desc\": \"AMI Measurement\",\n", + " \"link_local_addr\":\"10\",\n", + " \"link_remote_addr\":\"1024\",\n", + " },\n", + " \"test outstation 2\":{\"name\":\"test outstation 2\",\n", + " \"conversion_type\":\"Shark\",\n", + " \"CIM object\":\"632633\",\n", + " \"port\": \"19999\",\n", + " \"ip\": \"127.0.0.1\",\n", + " \"desc\": \"AMI Measurement\",\n", + " \"link_local_addr\":\"10\",\n", + " \"link_remote_addr\":\"1024\",\n", + " },\n", + " \"shark 1\":{\"name\":\"shark 1\",\n", + " \"sim_object\": \"line_632633\"\n", + " \"conversion_type\":\"Shark\",\n", + " \"CIM object\":\"632633\",\n", + " \"port\": \"20000\",\n", + " \"ip\": \"10.79.91.94\",\n", + " \"desc\": \"AMI Measurement\",\n", + " \"link_local_addr\":\"1\",\n", + " \"link_remote_addr\":\"0\",\n", + " },\n", + " \"shark 2\":{\"name\":\"shark 2\",\n", + " \"conversion_type\":\"Shark\",\n", + " \"CIM object\":\"632633\",\n", + " \"port\": \"20000\",\n", + " \"ip\": \"10.79.91.95\",\n", + " \"desc\": \"AMI Measurement\",\n", + " \"link_local_addr\":\"1\",\n", + " \"link_remote_addr\":\"0\",\n", + " },\n", + " \"capbank 1\":{\"name\":\"capbank 1\",\n", + " \"conversion_type\":\"Beckwith Capbank\",\n", + " \"CIM object\":\"cap1\",\n", + " \"port\": \"30004\",\n", + " \"ip\": \"10.79.91.70\",\n", + " \"desc\": \"AMI Measurement\",\n", + " \"link_local_addr\":\"100\",\n", + " \"link_remote_addr\":\"0\",\n", + " } \n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"device_ip_port_config_all.json\",\"w\") as f:\n", + " json.dump(device_ip_port_config_all,f,indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 45ad88c8244c1f1040f8a0e0b326e87feac572f3 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Mon, 23 Nov 2020 15:22:52 -0700 Subject: [PATCH 32/64] Added README.md --- test/README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 test/README.md diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..296ad3c --- /dev/null +++ b/test/README.md @@ -0,0 +1,27 @@ + + +### Notebooks and DNP3 list.xlsx + +The MappingExample.ipynb note book reads the "DNP3 list.xlsx" file and builds a json dictionary with the index information for each conversion type. +The device_ip_port_config_all.json is built by the ports.ipynb notebook.device + +Lookup device ip and port information in device_ip_port_config_all.json file. +The file is built by the ports.ipynb notebook.device + +The SOEHandler in the master.py will handle the indexes to CIM MRID mapping and conversion. +The master_main.py will start all the masters and collect the built CIM message from each master.SOEHandler and send to the FIM topic. + + +To test use the following. + +```bash +cd test + +python master_main.py 'test outstation 1' + +python outstation_main.py 'test outstation 1' + +``` + +TODO Alka +Complete integration of capbank in new_start_service and test with stand alone outstation_cmd or outstation_main. From fb4c67cf2c44557c195608041e5e2a6d7246ad31 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 18 Dec 2020 10:03:46 -0700 Subject: [PATCH 33/64] Update to put more of the the mapping information in the DNP3 list.xlsx file --- dnp3/service/dnp3/dnp3_to_cim.py | 99 +++- dnp3/service/dnp3/master.py | 73 ++- dnp3/service/dnp3/outstation.py | 65 ++- test/MappingExample.ipynb | 765 ++++++++++++++++++++++++------- 4 files changed, 782 insertions(+), 220 deletions(-) diff --git a/dnp3/service/dnp3/dnp3_to_cim.py b/dnp3/service/dnp3/dnp3_to_cim.py index eb99aa3..09909d6 100644 --- a/dnp3/service/dnp3/dnp3_to_cim.py +++ b/dnp3/service/dnp3/dnp3_to_cim.py @@ -49,7 +49,86 @@ def build_conversion(csv_file): # with open("conversion_dict.json", "w") as f: # json.dump(conversion_dict, f, indent=2) -def model_line_dict(model_dict_json): +def get_device_dict(model_dict, model_line_dict, device_type, name): + if device_type == 'Shark': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('ACLineSegment_'+name): + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + elif device_type == 'Beckwith CapBank 2': + #LinearShuntCompensator + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('LinearShuntCompensator_'+name): + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + elif meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'Pos': + if meas['measurementType'] not in model_line_dict[name]: + model_line_dict[name][meas['measurementType']] = {} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'pos'} + for cap in model_dict['feeders'][0]["capacitors"]: + if cap['name'] == name: + model_line_dict[name]['manual close'] = {'mrid':meas['mRID'],'type':'magnitude'} + print(cap) + #TODO figure this out + # 0 manual close + # 1 manual open + elif device_type == 'Beckwith LTC': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('RatioTapChanger_'+name):#ConductingEquipment_name + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + elif meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'Pos': + if meas['measurementType'] not in model_line_dict[name]: + model_line_dict[name][meas['measurementType']] = {} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'pos'} + for reg in model_dict['feeders'][0]["regulators"]: + if reg['bankName'] == name: + # Do I have to count for each position change? + print(reg) + +def model_line_dict(): + with open("model_dict.json") as f: + model_dict = json.load(f) + + line_name = "632633" + cap_name = "cap1" + reg_name = "Reg" + model_line_dict = {line_name: {}, + cap_name: {}, + reg_name: {}} + + name = line_name + device_type = "Shark" + get_device_dict(model_dict, model_line_dict, device_type, name) + device_type = "Beckwith CapBank 2" + name = "cap1" + get_device_dict(model_dict, model_line_dict, device_type, name) + device_type = 'Beckwith LTC' + name = "Reg" + get_device_dict(model_dict, model_line_dict, device_type, name) + + with open("model_line_dict.json", "w") as f: + json.dump(model_line_dict, f, indent=2) + +def model_line_dict_old(model_dict_json): from_node = '632' to_node = '633' node_name = '633' @@ -66,6 +145,22 @@ def model_line_dict(model_dict_json): device_type = "Shark" device_type = "Beckwith CapBank 2" device_type = 'Beckwith LTC' + # if device_type == 'Shark': + # for meas in model_dict['feeders'][0]['measurements']: + # if meas['name'].startswith('ACLineSegment_' + line_name): + # print(meas) + # if meas['measurementType'] == 'PNV': + # model_line_dict[from_node + to_node]['Voltage feedback ' + meas['phases'] + ' (L-N)'] = { + # 'mrid': meas['mRID'], 'type': 'magnitude'} + # model_line_dict[from_node + to_node]['Voltage feedback ' + meas['phases'] + ' (L-L)'] = { + # 'mrid': meas['mRID'], 'type': 'angle'} + # if meas['measurementType'] == 'VA': + # model_line_dict[from_node + to_node]['P ' + meas['phases']] = {'mrid': meas['mRID'], + # 'type': 'magnitude'} + # model_line_dict[from_node + to_node]['Q ' + meas['phases']] = {'mrid': meas['mRID'], + # 'type': 'angle'} + # # if meas['ConnectivityNode'] == node_name and meas['ConductingEquipment_type'] == 'ACLineSegment': + # # print(meas) if device_type == 'Shark': for meas in model_dict['feeders'][0]['measurements']: if meas['name'].startswith('ACLineSegment_' + line_name): @@ -80,8 +175,6 @@ def model_line_dict(model_dict_json): 'type': 'magnitude'} model_line_dict[from_node + to_node]['Q ' + meas['phases']] = {'mrid': meas['mRID'], 'type': 'angle'} - # if meas['ConnectivityNode'] == node_name and meas['ConductingEquipment_type'] == 'ACLineSegment': - # print(meas) elif device_type == 'Beckwith CapBank 2': # LinearShuntCompensator for meas in model_dict['feeders'][0]['measurements']: diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 918980d..b48399f 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -274,6 +274,40 @@ def __init__(self, name, device, dnp3_to_cim): def get_msg(self): return self._cim_msg + def update_cim_msg_analog(self, CIM_msg, index, value, conversion, model): + if 'Analog input' in conversion and index in conversion['Analog input']: + CIM_phase = conversion['Analog input'][index]['CIM phase'] + CIM_units = conversion['Analog input'][index]['CIM units'] + CIM_attribute = conversion['Analog input'][index]['CIM attribute'] + ## Check if multiplier is na or str + multiplier = conversion['Analog input'][index]['Multiplier'] + mrid = model[CIM_units][CIM_phase]['mrid'] + if type(multiplier) == str: + multiplier = 1 + + CIM_value = {'mrid': mrid} + if CIM_units == 'PNV' or CIM_units == 'VA': + CIM_value = {'mrid': mrid, 'magnitude': 0, 'angle': 0} + + if mrid not in CIM_msg: + CIM_msg[mrid] = CIM_value + CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier + + def update_cim_msg_binary(self, CIM_msg, index, value, conversion,model): + # print(conversion['Binary input'][index]) + if 'Binary input' in conversion and index in conversion['Binary input']: + CIM_phases = conversion['Binary input'][index]['CIM phase'] + CIM_units = conversion['Binary input'][index]['CIM units'] + CIM_attribute = conversion['Binary input'][index]['CIM attribute'] + ## Check if multiplier is na or str + multiplier = conversion['Binary input'][index]['Multiplier'] + for CIM_phase in CIM_phases: + mrid = model[CIM_units][CIM_phase]['mrid'] + CIM_value = {'mrid': mrid} + if mrid not in CIM_msg: + CIM_msg[mrid] = CIM_value + CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier + def Process(self, info, values): """ Process measurement data. @@ -300,6 +334,7 @@ def Process(self, info, values): model_line_dict = self._dnp3_to_cim.model_line_dict element_attr_to_mrid = model_line_dict[self._name] + model = model_line_dict[self._name] conversion = conversion_dict[self._device] # print(conversion) @@ -311,24 +346,25 @@ def Process(self, info, values): log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) if isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: - ## Check if multiplier is na or str - multiplier = 1 - conversion_for_index = conversion['Analog input'][str(float(index))] - if 'Multiplier' in conversion_for_index: - multiplier = conversion_for_index['Multiplier'] - if type(multiplier) != str: - multiplier = conversion_for_index['Multiplier'] - else: - print("No multiplier") - dnp3_attr_name = conversion_for_index['type'] - if dnp3_attr_name in element_attr_to_mrid: - # print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) - if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in self._cim_msg: - self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, - 'angle': 0, - 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} - value_type = element_attr_to_mrid[dnp3_attr_name]['type'] - self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier + self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) + # ## Check if multiplier is na or str + # multiplier = 1 + # conversion_for_index = conversion['Analog input'][str(float(index))] + # if 'Multiplier' in conversion_for_index: + # multiplier = conversion_for_index['Multiplier'] + # if type(multiplier) != str: + # multiplier = conversion_for_index['Multiplier'] + # else: + # print("No multiplier") + # dnp3_attr_name = conversion_for_index['type'] + # if dnp3_attr_name in element_attr_to_mrid: + # # print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) + # if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in self._cim_msg: + # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, + # 'angle': 0, + # 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} + # value_type = element_attr_to_mrid[dnp3_attr_name]['type'] + # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier else: print(" No entry for index " + str(index)) # print(cim_msg) @@ -338,6 +374,7 @@ def Process(self, info, values): print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work else: for index, value in visitor.index_and_value: print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) diff --git a/dnp3/service/dnp3/outstation.py b/dnp3/service/dnp3/outstation.py index 4b901e2..fb2c0ac 100755 --- a/dnp3/service/dnp3/outstation.py +++ b/dnp3/service/dnp3/outstation.py @@ -37,7 +37,13 @@ import json from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 +import sys +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + _log = logging.getLogger(__name__) +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) class DNP3Outstation(opendnp3.IOutstationApplication): @@ -74,7 +80,7 @@ class DNP3Outstation(opendnp3.IOutstationApplication): print(outstation_config) agent = None - def __init__(self, local_ip, port, outstation_config): + def __init__(self, local_ip, port, DNP3_ADDR, outstation_config): """ Initialize the outstation's Application Layer. @@ -93,6 +99,7 @@ def __init__(self, local_ip, port, outstation_config): super(DNP3Outstation, self).__init__() self.local_ip = local_ip self.port = port + self.DNP3_ADDR = DNP3_ADDR #print(local_ip), self.set_outstation_config(outstation_config) # The following variables are initialized after start() is called. @@ -123,27 +130,37 @@ def start(self): # link_addr = m['link_local_addr'] # remote_addr = m['link_remote_addr'] self.stack_config.link.LocalAddr = self.outstation_config.get('link_local_addr', 1) - self.stack_config.link.RemoteAddr = self.outstation_config.get('link_remote_addr',1024) + self.stack_config.link.LocalAddr = self.DNP3_ADDR + self.stack_config.link.RemoteAddr = self.outstation_config.get('link_remote_addr', 1024) + self.stack_config.link.RemoteAddr = self.DNP3_ADDR self.stack_config.link.KeepAliveTimeout = openpal.TimeDuration().Max() + self.stack_config.link.LocalAddr = 110 ## Was 10 + self.stack_config.link.RemoteAddr = 11 ## TODO Fix # Configure the outstation database of points based on the contents of the data dictionary. _log.debug('Configuring the DNP3 Outstation database.') db_config = self.stack_config.dbConfig _log.debug(db_config) - for point in self.get_agent().point_definitions.all_points(): - #print("Agent is", self.get_agent()) - #_log.debug("Adding Point: {}".format(point)) - if point.point_type == 'Analog Input': - cfg = db_config.analog[int(point.index)] - elif point.point_type == 'Binary Input': - cfg = db_config.binary[int(point.index)] - else: - # This database's point configuration is limited to Binary and Analog data types. - cfg = None - if cfg: - cfg.clazz = point.eclass - cfg.svariation = point.svariation - cfg.evariation = point.evariation + try: + for point in self.get_agent().point_definitions.all_points(): + #print("Agent is", self.get_agent()) + #_log.debug("Adding Point: {}".format(point)) + if point.point_type == 'Analog Input': + cfg = db_config.analog[int(point.index)] + elif point.point_type == 'Binary Input': + cfg = db_config.binary[int(point.index)] + else: + # This database's point configuration is limited to Binary and Analog data types. + cfg = None + if cfg: + cfg.clazz = point.eclass + cfg.svariation = point.svariation + cfg.evariation = point.evariation + except ValueError as err: + if False: + # if not os.environ.get('UNITTEST', False): + raise err + print("No agent") _log.debug('Creating a DNP3Manager.') threads_to_allocate = self.outstation_config.get('threads_to_allocate', 1) @@ -172,7 +189,7 @@ def start(self): int(self.port), self.listener) - + print("LocalAddr", self.stack_config.link.LocalAddr,"RemoteAddr",self.stack_config.link.RemoteAddr) #_log.debug(str(self.channel)) _log.debug('Adding the DNP3 Outstation to the channel.') # self.command_handler = opendnp3.SuccessCommandHandler().Create() # (or use this during regression testing) @@ -429,8 +446,11 @@ def OnStateChange(self, state): :param state: A ChannelState. """ - self.dnp3Object.get_agent().publish_outstation_status(str(state)) - #self.get_agent().publish_outstation_status(str(state)) + try: + self.dnp3Object.get_agent().publish_outstation_status(str(state)) + #self.get_agent().publish_outstation_status(str(state)) + except ValueError as err: + print("No Agent") # class MyLogger(asiodnp3.ConsoleLogger): @@ -450,8 +470,11 @@ def Log(self, entry): message = entry.message _log.debug('DNP3Log {0}\t(filters={1}) {2}'.format(location, filters, message)) # This is here as an example of how to send a specific log entry to the message bus as outstation status. - if 'Accepted connection' in message or 'Listening on' in message: - self.dnp3Object.get_agent().publish_outstation_status(str(message)) + try: + if 'Accepted connection' in message or 'Listening on' in message: + self.dnp3Object.get_agent().publish_outstation_status(str(message)) + except ValueError as err: + print("No agent") def main(): diff --git a/test/MappingExample.ipynb b/test/MappingExample.ipynb index 5578f38..fce1fb9 100644 --- a/test/MappingExample.ipynb +++ b/test/MappingExample.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 100, + "execution_count": 89, "metadata": {}, "outputs": [ { @@ -41,6 +41,9 @@ " Multiplier\n", " MultiplierString\n", " Units\n", + " CIM attribute\n", + " CIM phase\n", + " CIM Units\n", " \n", " \n", " \n", @@ -51,6 +54,9 @@ " NaN\n", " NaN\n", " NaN\n", + " NaN\n", + " NaN\n", + " NaN\n", " \n", " \n", " 1\n", @@ -59,6 +65,9 @@ " 0.004578\n", " (150 / 32768)\n", " V\n", + " magnitude\n", + " A\n", + " PNV\n", " \n", " \n", " 2\n", @@ -67,6 +76,9 @@ " 0.004578\n", " (150 / 32768)\n", " V\n", + " magnitude\n", + " B\n", + " PNV\n", " \n", " \n", " 3\n", @@ -75,6 +87,9 @@ " 0.004578\n", " (150 / 32768)\n", " V\n", + " magnitude\n", + " C\n", + " PNV\n", " \n", " \n", " 4\n", @@ -83,6 +98,9 @@ " 0.009155\n", " (300 / 32768)\n", " V\n", + " angle\n", + " A\n", + " PNV\n", " \n", " \n", " 5\n", @@ -91,6 +109,9 @@ " 0.009155\n", " (300 / 32768)\n", " V\n", + " angle\n", + " B\n", + " PNV\n", " \n", " \n", " 6\n", @@ -99,6 +120,9 @@ " 0.009155\n", " (300 / 32768)\n", " V\n", + " angle\n", + " C\n", + " PNV\n", " \n", " \n", " 7\n", @@ -107,6 +131,9 @@ " 0.000305\n", " (10 / 32768)\n", " A\n", + " NaN\n", + " A\n", + " PNV\n", " \n", " \n", " 8\n", @@ -115,6 +142,9 @@ " 0.000305\n", " (10 / 32768)\n", " A\n", + " NaN\n", + " B\n", + " PNV\n", " \n", " \n", " 9\n", @@ -123,6 +153,9 @@ " 0.000305\n", " (10 / 32768)\n", " A\n", + " NaN\n", + " C\n", + " PNV\n", " \n", " \n", " 10\n", @@ -131,6 +164,9 @@ " 0.137329\n", " (4500 / 32768)\n", " W\n", + " NaN\n", + " NaN\n", + " NaN\n", " \n", " \n", " 11\n", @@ -139,6 +175,9 @@ " 0.137329\n", " (4500 / 32768)\n", " VARs\n", + " NaN\n", + " NaN\n", + " NaN\n", " \n", " \n", " 12\n", @@ -147,6 +186,9 @@ " 0.137329\n", " (4500 / 32768)\n", " VA\n", + " NaN\n", + " NaN\n", + " NaN\n", " \n", " \n", " 13\n", @@ -155,6 +197,9 @@ " 0.001000\n", " 0.001\n", " NaN\n", + " NaN\n", + " NaN\n", + " NaN\n", " \n", " \n", " 14\n", @@ -163,6 +208,9 @@ " 0.010000\n", " 0.01\n", " Hz\n", + " NaN\n", + " NaN\n", + " NaN\n", " \n", " \n", " 15\n", @@ -171,6 +219,9 @@ " 0.137329\n", " (4500/32768)\n", " W\n", + " real\n", + " A\n", + " VA\n", " \n", " \n", " 16\n", @@ -179,6 +230,9 @@ " 0.137329\n", " (4500/32768)\n", " W\n", + " real\n", + " B\n", + " VA\n", " \n", " \n", " 17\n", @@ -187,6 +241,9 @@ " 0.137329\n", " (4500/32768)\n", " W\n", + " real\n", + " C\n", + " VA\n", " \n", " \n", " 18\n", @@ -195,6 +252,9 @@ " 0.137329\n", " (4500/32768)\n", " VARs\n", + " imag\n", + " A\n", + " VA\n", " \n", " \n", " 19\n", @@ -203,6 +263,9 @@ " 0.137329\n", " (4500/32768)\n", " VARs\n", + " imag\n", + " B\n", + " VA\n", " \n", " \n", " 20\n", @@ -211,37 +274,63 @@ " 0.137329\n", " (4500/32768)\n", " VARs\n", + " iamg\n", + " C\n", + " VA\n", " \n", " \n", "\n", "" ], "text/plain": [ - " Index type Multiplier MultiplierString Units\n", - "0 NaN Analog input NaN NaN NaN\n", - "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V\n", - "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V\n", - "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V\n", - "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V\n", - "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V\n", - "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V\n", - "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A\n", - "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A\n", - "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A\n", - "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W\n", - "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs\n", - "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA\n", - "13 13.0 PF 0.001000 0.001 NaN\n", - "14 13.0 Hz 0.010000 0.01 Hz\n", - "15 36.0 P A 0.137329 (4500/32768) W\n", - "16 37.0 P B 0.137329 (4500/32768) W\n", - "17 38.0 P C 0.137329 (4500/32768) W\n", - "18 39.0 Q A 0.137329 (4500/32768) VARs\n", - "19 40.0 Q B 0.137329 (4500/32768) VARs\n", - "20 41.0 Q C 0.137329 (4500/32768) VARs" + " Index type Multiplier MultiplierString Units \\\n", + "0 NaN Analog input NaN NaN NaN \n", + "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", + "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", + "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", + "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", + "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V \n", + "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V \n", + "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A \n", + "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A \n", + "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A \n", + "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W \n", + "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", + "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", + "13 13.0 PF 0.001000 0.001 NaN \n", + "14 13.0 Hz 0.010000 0.01 Hz \n", + "15 36.0 P A 0.137329 (4500/32768) W \n", + "16 37.0 P B 0.137329 (4500/32768) W \n", + "17 38.0 P C 0.137329 (4500/32768) W \n", + "18 39.0 Q A 0.137329 (4500/32768) VARs \n", + "19 40.0 Q B 0.137329 (4500/32768) VARs \n", + "20 41.0 Q C 0.137329 (4500/32768) VARs \n", + "\n", + " CIM attribute CIM phase CIM Units \n", + "0 NaN NaN NaN \n", + "1 magnitude A PNV \n", + "2 magnitude B PNV \n", + "3 magnitude C PNV \n", + "4 angle A PNV \n", + "5 angle B PNV \n", + "6 angle C PNV \n", + "7 NaN A PNV \n", + "8 NaN B PNV \n", + "9 NaN C PNV \n", + "10 NaN NaN NaN \n", + "11 NaN NaN NaN \n", + "12 NaN NaN NaN \n", + "13 NaN NaN NaN \n", + "14 NaN NaN NaN \n", + "15 real A VA \n", + "16 real B VA \n", + "17 real C VA \n", + "18 imag A VA \n", + "19 imag B VA \n", + "20 iamg C VA " ] }, - "execution_count": 100, + "execution_count": 89, "metadata": {}, "output_type": "execute_result" } @@ -253,7 +342,7 @@ }, { "cell_type": "code", - "execution_count": 101, + "execution_count": 90, "metadata": {}, "outputs": [], "source": [ @@ -263,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 102, + "execution_count": 91, "metadata": {}, "outputs": [ { @@ -280,86 +369,146 @@ "{nan: {'type': 'Analog input',\n", " 'Multiplier': nan,\n", " 'MultiplierString': nan,\n", - " 'Units': nan},\n", + " 'Units': nan,\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM Units': nan},\n", " 1.0: {'type': 'Voltage feedback A (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'A',\n", + " 'CIM Units': 'PNV'},\n", " 2.0: {'type': 'Voltage feedback B (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'B',\n", + " 'CIM Units': 'PNV'},\n", " 3.0: {'type': 'Voltage feedback C (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'C',\n", + " 'CIM Units': 'PNV'},\n", " 4.0: {'type': 'Voltage feedback A (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'A',\n", + " 'CIM Units': 'PNV'},\n", " 5.0: {'type': 'Voltage feedback B (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'B',\n", + " 'CIM Units': 'PNV'},\n", " 6.0: {'type': 'Voltage feedback C (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'C',\n", + " 'CIM Units': 'PNV'},\n", " 7.0: {'type': 'Current feedback (A)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", - " 'Units': 'A'},\n", + " 'Units': 'A',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': 'A',\n", + " 'CIM Units': 'PNV'},\n", " 8.0: {'type': 'Current feedback (B)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", - " 'Units': 'A'},\n", + " 'Units': 'A',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': 'B',\n", + " 'CIM Units': 'PNV'},\n", " 9.0: {'type': 'Current feedback (C)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", - " 'Units': 'A'},\n", + " 'Units': 'A',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': 'C',\n", + " 'CIM Units': 'PNV'},\n", " 10.0: {'type': 'Total 3 ph P',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM Units': nan},\n", " 11.0: {'type': 'Total 3 ph Q',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", - " 'Units': 'VARs'},\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM Units': nan},\n", " 12.0: {'type': 'Total 3 ph VA',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", - " 'Units': 'VA'},\n", + " 'Units': 'VA',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM Units': nan},\n", " 13.0: {'type': 'Hz',\n", " 'Multiplier': 0.01,\n", " 'MultiplierString': 0.01,\n", - " 'Units': 'Hz'},\n", + " 'Units': 'Hz',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM Units': nan},\n", " 36.0: {'type': 'P A',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'A',\n", + " 'CIM Units': 'VA'},\n", " 37.0: {'type': 'P B',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'B',\n", + " 'CIM Units': 'VA'},\n", " 38.0: {'type': 'P C',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'C',\n", + " 'CIM Units': 'VA'},\n", " 39.0: {'type': 'Q A',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'VARs'},\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'A',\n", + " 'CIM Units': 'VA'},\n", " 40.0: {'type': 'Q B',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'VARs'},\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'B',\n", + " 'CIM Units': 'VA'},\n", " 41.0: {'type': 'Q C',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'VARs'}}" + " 'Units': 'VARs',\n", + " 'CIM attribute': 'iamg',\n", + " 'CIM phase': 'C',\n", + " 'CIM Units': 'VA'}}" ] }, - "execution_count": 102, + "execution_count": 91, "metadata": {}, "output_type": "execute_result" } @@ -372,7 +521,7 @@ }, { "cell_type": "code", - "execution_count": 103, + "execution_count": 92, "metadata": {}, "outputs": [], "source": [ @@ -383,7 +532,7 @@ }, { "cell_type": "code", - "execution_count": 104, + "execution_count": 93, "metadata": {}, "outputs": [], "source": [ @@ -406,7 +555,7 @@ }, { "cell_type": "code", - "execution_count": 105, + "execution_count": 94, "metadata": {}, "outputs": [ { @@ -502,38 +651,83 @@ }, { "cell_type": "code", - "execution_count": 106, + "execution_count": 95, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'name': 'ACLineSegment_632633_Power', 'mRID': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C', 'measurementType': 'VA', 'phases': 'C', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '632', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Power', 'mRID': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C', 'measurementType': 'VA', 'phases': 'A', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '632', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Voltage', 'mRID': '_5bc367a5-f771-494a-83bd-b4560d538df7', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_F0E10483-D8AD-46BE-AF5F-08228955796B', 'measurementType': 'PNV', 'phases': 'C', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '633', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Voltage', 'mRID': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_F0E10483-D8AD-46BE-AF5F-08228955796B', 'measurementType': 'PNV', 'phases': 'A', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '633', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Voltage', 'mRID': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_F0E10483-D8AD-46BE-AF5F-08228955796B', 'measurementType': 'PNV', 'phases': 'B', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '633', 'SimObject': 'line_632633'}\n", + "{'name': 'ACLineSegment_632633_Power', 'mRID': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'ConductingEquipment_mRID': '_FBE26B35-13AB-457D-9795-DF58B28E309D', 'Terminal_mRID': '_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C', 'measurementType': 'VA', 'phases': 'B', 'MeasurementClass': 'Analog', 'ConductingEquipment_type': 'ACLineSegment', 'ConductingEquipment_name': '632633', 'ConnectivityNode': '632', 'SimObject': 'line_632633'}\n" + ] + } + ], + "source": [ + "import json\n", + "from_node = '632'\n", + "to_node = '633'\n", + "node_name='633'\n", + "\n", + "line_name = from_node+to_node\n", + "cap_name = \"cap1\"\n", + "reg_name = \"Reg\"\n", + "name = \"632633\"\n", + "\n", + "with open(\"model_dict.json\") as f:\n", + " model_dict = json.load(f)\n", + "\n", + "model_line_dict={line_name:{},\n", + " cap_name:{},\n", + " reg_name:{}}\n", + "device_type = \"Shark\"\n", + "# device_type= \"Beckwith CapBank 2\"\n", + "# device_type = 'Beckwith LTC'\n", + "if device_type == 'Shark':\n", + " for meas in model_dict['feeders'][0]['measurements']:\n", + " if meas['name'].startswith('ACLineSegment_'+line_name):\n", + " print(meas)\n", + " if meas['measurementType'] == 'PNV':\n", + " if meas['measurementType'] not in model_line_dict[from_node+to_node]: model_line_dict[from_node+to_node][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " if meas['measurementType'] == 'VA':\n", + " if meas['measurementType'] not in model_line_dict[from_node+to_node]: model_line_dict[from_node+to_node][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + "\n", + "\n", + " \n", + "model_line_dict\n", + "with open(\"model_line_dict.json\",\"w\") as f:\n", + " json.dump(model_line_dict,f,indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'632633': {'P C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", - " 'type': 'magnitude'},\n", - " 'Q C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'angle'},\n", - " 'P A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2',\n", - " 'type': 'magnitude'},\n", - " 'Q A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", - " 'Voltage feedback C (L-N)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback C (L-L)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", - " 'type': 'angle'},\n", - " 'Voltage feedback A (L-N)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback A (L-L)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", - " 'type': 'angle'},\n", - " 'Voltage feedback B (L-N)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback B (L-L)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", - " 'type': 'angle'},\n", - " 'P B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30',\n", - " 'type': 'magnitude'},\n", - " 'Q B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + "{'632633': {'VA': {'C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + " 'PNV': {'C': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'type': 'angle'},\n", + " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}},\n", " 'cap1': {},\n", " 'Reg': {}}" ] }, - "execution_count": 106, + "execution_count": 96, "metadata": {}, "output_type": "execute_result" } @@ -544,7 +738,7 @@ }, { "cell_type": "code", - "execution_count": 107, + "execution_count": 97, "metadata": {}, "outputs": [], "source": [ @@ -558,7 +752,7 @@ }, { "cell_type": "code", - "execution_count": 108, + "execution_count": 98, "metadata": {}, "outputs": [], "source": [ @@ -585,33 +779,23 @@ }, { "cell_type": "code", - "execution_count": 109, + "execution_count": 99, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'P C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'magnitude'},\n", - " 'Q C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e', 'type': 'angle'},\n", - " 'P A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'magnitude'},\n", - " 'Q A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", - " 'Voltage feedback C (L-N)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback C (L-L)': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", - " 'type': 'angle'},\n", - " 'Voltage feedback A (L-N)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback A (L-L)': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", - " 'type': 'angle'},\n", - " 'Voltage feedback B (L-N)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", - " 'type': 'magnitude'},\n", - " 'Voltage feedback B (L-L)': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", - " 'type': 'angle'},\n", - " 'P B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'magnitude'},\n", - " 'Q B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}}" + "{'VA': {'C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + " 'PNV': {'C': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'type': 'angle'},\n", + " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}}" ] }, - "execution_count": 109, + "execution_count": 99, "metadata": {}, "output_type": "execute_result" } @@ -622,7 +806,7 @@ }, { "cell_type": "code", - "execution_count": 110, + "execution_count": 100, "metadata": {}, "outputs": [], "source": [ @@ -631,7 +815,7 @@ }, { "cell_type": "code", - "execution_count": 111, + "execution_count": 101, "metadata": {}, "outputs": [ { @@ -641,7 +825,7 @@ "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexclusion_dict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_csv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"exclusion.csv\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexclusion_dict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_csv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"exclusion.csv\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mNameError\u001b[0m: name 'exclusion_dict' is not defined" ] } @@ -652,7 +836,7 @@ }, { "cell_type": "code", - "execution_count": 112, + "execution_count": 102, "metadata": {}, "outputs": [], "source": [ @@ -695,7 +879,7 @@ }, { "cell_type": "code", - "execution_count": 113, + "execution_count": 103, "metadata": {}, "outputs": [], "source": [ @@ -704,7 +888,7 @@ }, { "cell_type": "code", - "execution_count": 114, + "execution_count": 115, "metadata": {}, "outputs": [ { @@ -713,27 +897,49 @@ "text": [ "Analog input\n", "1 21\n", - " Index type Multiplier MultiplierString Units\n", - "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V\n", - "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V\n", - "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V\n", - "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V\n", - "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V\n", - "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V\n", - "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A\n", - "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A\n", - "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A\n", - "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W\n", - "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs\n", - "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA\n", - "13 13.0 PF 0.001000 0.001 NaN\n", - "14 13.0 Hz 0.010000 0.01 Hz\n", - "15 36.0 P A 0.137329 (4500/32768) W\n", - "16 37.0 P B 0.137329 (4500/32768) W\n", - "17 38.0 P C 0.137329 (4500/32768) W\n", - "18 39.0 Q A 0.137329 (4500/32768) VARs\n", - "19 40.0 Q B 0.137329 (4500/32768) VARs\n", - "20 41.0 Q C 0.137329 (4500/32768) VARs\n" + " Index type Multiplier MultiplierString Units \\\n", + "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", + "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", + "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", + "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", + "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V \n", + "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V \n", + "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A \n", + "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A \n", + "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A \n", + "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W \n", + "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", + "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", + "13 13.0 PF 0.001000 0.001 NaN \n", + "14 13.0 Hz 0.010000 0.01 Hz \n", + "15 36.0 P A 0.137329 (4500/32768) W \n", + "16 37.0 P B 0.137329 (4500/32768) W \n", + "17 38.0 P C 0.137329 (4500/32768) W \n", + "18 39.0 Q A 0.137329 (4500/32768) VARs \n", + "19 40.0 Q B 0.137329 (4500/32768) VARs \n", + "20 41.0 Q C 0.137329 (4500/32768) VARs \n", + "\n", + " CIM attribute CIM phase CIM units \n", + "1 magnitude A PNV \n", + "2 magnitude B PNV \n", + "3 magnitude C PNV \n", + "4 angle A PNV \n", + "5 angle B PNV \n", + "6 angle C PNV \n", + "7 NaN A PNV \n", + "8 NaN B PNV \n", + "9 NaN C PNV \n", + "10 NaN NaN NaN \n", + "11 NaN NaN NaN \n", + "12 NaN NaN NaN \n", + "13 NaN NaN NaN \n", + "14 NaN NaN NaN \n", + "15 real A VA \n", + "16 real B VA \n", + "17 real C VA \n", + "18 imag A VA \n", + "19 imag B VA \n", + "20 iamg C VA \n" ] }, { @@ -751,7 +957,7 @@ }, { "cell_type": "code", - "execution_count": 115, + "execution_count": 116, "metadata": {}, "outputs": [ { @@ -760,82 +966,139 @@ "{1.0: {'type': 'Voltage feedback A (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", " 2.0: {'type': 'Voltage feedback B (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", " 3.0: {'type': 'Voltage feedback C (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", " 4.0: {'type': 'Voltage feedback A (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", " 5.0: {'type': 'Voltage feedback B (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", " 6.0: {'type': 'Voltage feedback C (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", - " 'Units': 'V'},\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", " 7.0: {'type': 'Current feedback (A)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", - " 'Units': 'A'},\n", + " 'Units': 'A',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", " 8.0: {'type': 'Current feedback (B)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", - " 'Units': 'A'},\n", + " 'Units': 'A',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", " 9.0: {'type': 'Current feedback (C)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", - " 'Units': 'A'},\n", + " 'Units': 'A',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", " 10.0: {'type': 'Total 3 ph P',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", " 11.0: {'type': 'Total 3 ph Q',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", - " 'Units': 'VARs'},\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", " 12.0: {'type': 'Total 3 ph VA',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", - " 'Units': 'VA'},\n", + " 'Units': 'VA',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", " 13.0: {'type': 'Hz',\n", " 'Multiplier': 0.01,\n", " 'MultiplierString': 0.01,\n", - " 'Units': 'Hz'},\n", + " 'Units': 'Hz',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", " 36.0: {'type': 'P A',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'VA'},\n", " 37.0: {'type': 'P B',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'VA'},\n", " 38.0: {'type': 'P C',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'W'},\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'VA'},\n", " 39.0: {'type': 'Q A',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'VARs'},\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'VA'},\n", " 40.0: {'type': 'Q B',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'VARs'},\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'VA'},\n", " 41.0: {'type': 'Q C',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", - " 'Units': 'VARs'}}" + " 'Units': 'VARs',\n", + " 'CIM attribute': 'iamg',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'VA'}}" ] }, - "execution_count": 115, + "execution_count": 116, "metadata": {}, "output_type": "execute_result" } @@ -846,7 +1109,7 @@ }, { "cell_type": "code", - "execution_count": 116, + "execution_count": 117, "metadata": {}, "outputs": [ { @@ -941,7 +1204,7 @@ }, { "cell_type": "code", - "execution_count": 117, + "execution_count": 118, "metadata": {}, "outputs": [ { @@ -969,6 +1232,9 @@ " Multiplier\n", " MultiplierString\n", " Units\n", + " CIM attribute\n", + " CIM phase\n", + " CIM Units\n", " \n", " \n", " Index\n", @@ -976,6 +1242,9 @@ " \n", " \n", " \n", + " \n", + " \n", + " \n", " \n", " \n", " \n", @@ -985,6 +1254,9 @@ " 0.004578\n", " (150 / 32768)\n", " V\n", + " magnitude\n", + " A\n", + " PNV\n", " \n", " \n", " 2.0\n", @@ -992,6 +1264,9 @@ " 0.004578\n", " (150 / 32768)\n", " V\n", + " magnitude\n", + " B\n", + " PNV\n", " \n", " \n", " 3.0\n", @@ -999,6 +1274,9 @@ " 0.004578\n", " (150 / 32768)\n", " V\n", + " magnitude\n", + " C\n", + " PNV\n", " \n", " \n", " 4.0\n", @@ -1006,21 +1284,31 @@ " 0.009155\n", " (300 / 32768)\n", " V\n", + " angle\n", + " A\n", + " PNV\n", " \n", " \n", "\n", "" ], "text/plain": [ - " type Multiplier MultiplierString Units\n", - "Index \n", - "1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V\n", - "2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V\n", - "3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V\n", - "4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V" + " type Multiplier MultiplierString Units \\\n", + "Index \n", + "1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", + "2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", + "3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", + "4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", + "\n", + " CIM attribute CIM phase CIM Units \n", + "Index \n", + "1.0 magnitude A PNV \n", + "2.0 magnitude B PNV \n", + "3.0 magnitude C PNV \n", + "4.0 angle A PNV " ] }, - "execution_count": 117, + "execution_count": 118, "metadata": {}, "output_type": "execute_result" } @@ -1031,7 +1319,7 @@ }, { "cell_type": "code", - "execution_count": 118, + "execution_count": 119, "metadata": {}, "outputs": [ { @@ -1054,7 +1342,7 @@ " 'Variation': nan}}" ] }, - "execution_count": 118, + "execution_count": 119, "metadata": {}, "output_type": "execute_result" } @@ -1065,7 +1353,7 @@ }, { "cell_type": "code", - "execution_count": 119, + "execution_count": 120, "metadata": {}, "outputs": [ { @@ -1078,7 +1366,7 @@ " 'Variation': nan}" ] }, - "execution_count": 119, + "execution_count": 120, "metadata": {}, "output_type": "execute_result" } @@ -1096,25 +1384,19 @@ }, { "cell_type": "code", - "execution_count": 120, + "execution_count": 121, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{'_806d5de7-541b-4944-8825-c7afffa5abc2': {'magnitude': 128.21044897368,\n", - " 'angle': 0.0010070800773},\n", - " '_bd5f3945-5d16-4694-896e-92da8a0e4895': {'magnitude': 0.0, 'angle': 0.0},\n", - " '_5bc367a5-f771-494a-83bd-b4560d538df7': {'magnitude': 0.0, 'angle': 0.0},\n", - " '_4044e33a-ecc2-4d3d-8584-f65643c4eea2': {'magnitude': 27.465820311999998,\n", - " 'angle': 0.0164794921872},\n", - " '_ebd287b8-bceb-4471-a709-c82fbd34bd30': {'magnitude': 0.0, 'angle': 0.0},\n", - " '_11bf885c-b677-42ba-859c-d1a013a9f36e': {'magnitude': 0.0, 'angle': 0.0}}" - ] - }, - "execution_count": 120, - "metadata": {}, - "output_type": "execute_result" + "ename": "KeyError", + "evalue": "'Voltage feedback A (L-N)'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;31m# print(meas_type,model[meas_type])\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 18\u001b[0;31m \u001b[0;32mif\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdnp3_attr_name\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'mrid'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mcim_msg\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 19\u001b[0m \u001b[0mcim_msg\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdnp3_attr_name\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'mrid'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m'magnitude'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m'angle'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0mvalue_type\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdnp3_attr_name\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'type'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mKeyError\u001b[0m: 'Voltage feedback A (L-N)'" + ] } ], "source": [ @@ -1128,7 +1410,7 @@ "\n", " for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", " value = values[index]\n", - " dnp3_attr_name = conversion['Analog input'][index]['type']\n", + " dnp3_attr_name = conversion['Analog input'][index]['type'] \n", " ## Check if multiplier is na or str\n", " multiplier = conversion['Analog input'][index]['Multiplier']\n", " if type(multiplier) == str:\n", @@ -1146,24 +1428,151 @@ }, { "cell_type": "code", - "execution_count": 121, + "execution_count": 144, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_806d5de7-541b-4944-8825-c7afffa5abc2': {'magnitude': 128.21044897368,\n", + " 'angle': 0.0010070800773},\n", + " '_bd5f3945-5d16-4694-896e-92da8a0e4895': {'magnitude': 0.0, 'angle': 0.0},\n", + " '_5bc367a5-f771-494a-83bd-b4560d538df7': {'magnitude': 0.0, 'angle': 0.0},\n", + " '_4044e33a-ecc2-4d3d-8584-f65643c4eea2': {'magnitude': 0,\n", + " 'angle': 0,\n", + " 'real': 27.465820311999998,\n", + " 'imag': 0.0164794921872},\n", + " '_ebd287b8-bceb-4471-a709-c82fbd34bd30': {'magnitude': 0,\n", + " 'angle': 0,\n", + " 'real': 0.0,\n", + " 'imag': 0.0},\n", + " '_11bf885c-b677-42ba-859c-d1a013a9f36e': {'magnitude': 0,\n", + " 'angle': 0,\n", + " 'real': 0.0,\n", + " 'iamg': 0.0}}" + ] + }, + "execution_count": 144, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "index = 1\n", + "values = {1:28008,2:0,3:0,4:0.11,5:0,6:0,36:200,37:0,38:0,39:0.12,40:0,41:0}\n", + "CIM_msg = {}\n", + "\n", + "for name, device in element_to_device.items():\n", + " model = model_line_dict[name]\n", + " conversion = conversion_dict[device]\n", + "\n", + " for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", + " value = values[index]\n", + " CIM_phase = conversion['Analog input'][index]['CIM phase']\n", + " CIM_units = conversion['Analog input'][index]['CIM units']\n", + " CIM_attribute = conversion['Analog input'][index]['CIM attribute']\n", + " ## Check if multiplier is na or str\n", + " multiplier = conversion['Analog input'][index]['Multiplier']\n", + " mrid = model[CIM_units][CIM_phase]['mrid']\n", + " if type(multiplier) == str:\n", + " multiplier = 1\n", + " \n", + " CIM_value = {'mrid':mrid,'value':0} \n", + " if CIM_units == 'PNV' or CIM_units == 'VA':\n", + " CIM_value = {'mrid':mrid, 'magnitude':0,'angle':0} \n", + " \n", + " if mrid not in CIM_msg:\n", + " CIM_msg[mrid] = CIM_value \n", + " CIM_msg[mrid][CIM_attribute] = value * multiplier # times multipier\n", + "\n", + "CIM_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 145, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'VA': {'C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + " 'PNV': {'C': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'type': 'angle'},\n", + " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}}" + ] + }, + "execution_count": 145, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model" + ] + }, + { + "cell_type": "code", + "execution_count": 146, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "0.13732910156" + "'angle'" ] }, - "execution_count": 121, + "execution_count": 146, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "conversion['Analog input'][index]['Multiplier']" + "value_type" ] }, + { + "cell_type": "code", + "execution_count": 124, + "metadata": {}, + "outputs": [], + "source": [ + "cim_phase = conversion['Analog input'][index]['CIM phase']\n", + "cim_units = conversion['Analog input'][index]['CIM units']\n", + "cim_attribute = conversion['Analog input'][index]['CIM attribute']" + ] + }, + { + "cell_type": "code", + "execution_count": 138, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'VA'" + ] + }, + "execution_count": 138, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cim_units" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, From 2f08d90235f694a664b1b18c885b27bc0433d959 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 5 Jan 2021 15:20:30 -0700 Subject: [PATCH 34/64] Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- test/Mapping_CIM_Attribute.ipynb | 1676 ++++++++++++++++++++++++++++++ 1 file changed, 1676 insertions(+) create mode 100644 test/Mapping_CIM_Attribute.ipynb diff --git a/test/Mapping_CIM_Attribute.ipynb b/test/Mapping_CIM_Attribute.ipynb new file mode 100644 index 0000000..499d288 --- /dev/null +++ b/test/Mapping_CIM_Attribute.ipynb @@ -0,0 +1,1676 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import json\n" + ] + }, + { + "cell_type": "code", + "execution_count": 176, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
IndextypeMultiplierMultiplierStringUnitsCIM attributeCIM phaseCIM units
0NaNAnalog inputNaNNaNNaNNaNNaNNaN
11.0Voltage feedback A (L-N)0.004578(150 / 32768)VmagnitudeAPNV
22.0Voltage feedback B (L-N)0.004578(150 / 32768)VmagnitudeBPNV
33.0Voltage feedback C (L-N)0.004578(150 / 32768)VmagnitudeCPNV
44.0Voltage feedback A (L-L)0.009155(300 / 32768)VangleAPNV
55.0Voltage feedback B (L-L)0.009155(300 / 32768)VangleBPNV
66.0Voltage feedback C (L-L)0.009155(300 / 32768)VangleCPNV
77.0Current feedback (A)0.000305(10 / 32768)AxAPNV
88.0Current feedback (B)0.000305(10 / 32768)AyBPNV
99.0Current feedback (C)0.000305(10 / 32768)AzCPNV
1010.0Total 3 ph P0.137329(4500 / 32768)WNaNNaNNaN
1111.0Total 3 ph Q0.137329(4500 / 32768)VARsNaNNaNNaN
1212.0Total 3 ph VA0.137329(4500 / 32768)VANaNNaNNaN
1313.0PF0.0010000.001NaNNaNNaNNaN
1413.0Hz0.0100000.01HzNaNNaNNaN
1536.0P A0.137329(4500/32768)WrealAVA
1637.0P B0.137329(4500/32768)WrealBVA
1738.0P C0.137329(4500/32768)WrealCVA
1839.0Q A0.137329(4500/32768)VARsimagAVA
1940.0Q B0.137329(4500/32768)VARsimagBVA
2041.0Q C0.137329(4500/32768)VARsiamgCVA
\n", + "
" + ], + "text/plain": [ + " Index type Multiplier MultiplierString Units \\\n", + "0 NaN Analog input NaN NaN NaN \n", + "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", + "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", + "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", + "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", + "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V \n", + "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V \n", + "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A \n", + "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A \n", + "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A \n", + "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W \n", + "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", + "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", + "13 13.0 PF 0.001000 0.001 NaN \n", + "14 13.0 Hz 0.010000 0.01 Hz \n", + "15 36.0 P A 0.137329 (4500/32768) W \n", + "16 37.0 P B 0.137329 (4500/32768) W \n", + "17 38.0 P C 0.137329 (4500/32768) W \n", + "18 39.0 Q A 0.137329 (4500/32768) VARs \n", + "19 40.0 Q B 0.137329 (4500/32768) VARs \n", + "20 41.0 Q C 0.137329 (4500/32768) VARs \n", + "\n", + " CIM attribute CIM phase CIM units \n", + "0 NaN NaN NaN \n", + "1 magnitude A PNV \n", + "2 magnitude B PNV \n", + "3 magnitude C PNV \n", + "4 angle A PNV \n", + "5 angle B PNV \n", + "6 angle C PNV \n", + "7 x A PNV \n", + "8 y B PNV \n", + "9 z C PNV \n", + "10 NaN NaN NaN \n", + "11 NaN NaN NaN \n", + "12 NaN NaN NaN \n", + "13 NaN NaN NaN \n", + "14 NaN NaN NaN \n", + "15 real A VA \n", + "16 real B VA \n", + "17 real C VA \n", + "18 imag A VA \n", + "19 imag B VA \n", + "20 iamg C VA " + ] + }, + "execution_count": 176, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.read_excel (r'DNP3 list.xlsx', sheet_name='Shark')\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 177, + "metadata": {}, + "outputs": [], + "source": [ + "conversion_dict = {}\n", + "df = df.set_index('Index')" + ] + }, + { + "cell_type": "code", + "execution_count": 178, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jsimpson/.conda/envs/adms36/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: DataFrame columns are not unique, some columns will be omitted.\n", + " \"\"\"Entry point for launching an IPython kernel.\n" + ] + }, + { + "data": { + "text/plain": [ + "{nan: {'type': 'Analog input',\n", + " 'Multiplier': nan,\n", + " 'MultiplierString': nan,\n", + " 'Units': nan,\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 1.0: {'type': 'Voltage feedback A (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", + " 2.0: {'type': 'Voltage feedback B (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", + " 3.0: {'type': 'Voltage feedback C (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", + " 4.0: {'type': 'Voltage feedback A (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", + " 5.0: {'type': 'Voltage feedback B (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", + " 6.0: {'type': 'Voltage feedback C (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", + " 7.0: {'type': 'Current feedback (A)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", + " 'Units': 'A',\n", + " 'CIM attribute': 'x',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", + " 8.0: {'type': 'Current feedback (B)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", + " 'Units': 'A',\n", + " 'CIM attribute': 'y',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", + " 9.0: {'type': 'Current feedback (C)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", + " 'Units': 'A',\n", + " 'CIM attribute': 'z',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", + " 10.0: {'type': 'Total 3 ph P',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", + " 'Units': 'W',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 11.0: {'type': 'Total 3 ph Q',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 12.0: {'type': 'Total 3 ph VA',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", + " 'Units': 'VA',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 13.0: {'type': 'Hz',\n", + " 'Multiplier': 0.01,\n", + " 'MultiplierString': 0.01,\n", + " 'Units': 'Hz',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 36.0: {'type': 'P A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'VA'},\n", + " 37.0: {'type': 'P B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'VA'},\n", + " 38.0: {'type': 'P C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'VA'},\n", + " 39.0: {'type': 'Q A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'VA'},\n", + " 40.0: {'type': 'Q B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'VA'},\n", + " 41.0: {'type': 'Q C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'iamg',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'VA'}}" + ] + }, + "execution_count": 178, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "shark_dict = df.T.to_dict()\n", + "conversion_dict['Shark'] = shark_dict\n", + "shark_dict" + ] + }, + { + "cell_type": "code", + "execution_count": 179, + "metadata": {}, + "outputs": [], + "source": [ + "conversion_dict = {\"Shark\":shark_dict}\n", + "# with open(\"conversion_dict.json\",\"w\") as f:\n", + "# json.dump(conversion_dict,f,indent=2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 180, + "metadata": {}, + "outputs": [], + "source": [ + "# Cap example\n", + "# new Capacitor.cap1 phases=3 bus1=675.1.2.3 conn=w kv=4.16 kvar=600.00\n", + "# {\"name\":\"cap1\",\"mRID\":\"_EAC7564D-2126-4138-8525-26B63DB6FDEE\",\"CN1\":\"675\",\"phases\":\"ABC\",\"kvar_A\":200.0,\"kvar_B\":200.0,\"kvar_C\":200.0,\"nominalVoltage\":4160.0,\"nomU\":4160.0,\"phaseConnection\":\"Y\",\"grounded\":true,\"enabled\":false,\"mode\":null,\"targetValue\":0.0,\"targetDeadband\":0.0,\"aVRDelay\":0.0,\"monitoredName\":null,\"monitoredClass\":null,\"monitoredBus\":null,\"monitoredPhase\":null},\n", + "# {\"name\":\"LinearShuntCompensator_cap1\",\"mRID\":\"_0df0fb2a-6df4-4020-a545-37ca57a6570d\",\"ConductingEquipment_mRID\":\"_EAC7564D-2126-4138-8525-26B63DB6FDEE\",\"Terminal_mRID\":\"_B7CE5B58-0F81-459A-BC50-47359A840565\",\"measurementType\":\"PNV\",\"phases\":\"B\",\"MeasurementClass\":\"Analog\",\"ConductingEquipment_type\":\"LinearShuntCompensator\",\"ConductingEquipment_name\":\"cap1\",\"ConnectivityNode\":\"675\",\"SimObject\":\"cap_cap1\"},\n", + "\n", + "# Reg example\n", + "# \"regulators\":[\n", + "#{\"bankName\":\"Reg\",\"size\":\"3\",\"bankPhases\":\"ABC\",\"tankName\":[\"reg1\",\"reg2\",\"reg3\"],\"endNumber\":[2,2,2],\"endPhase\":[\"A\",\"B\",\"C\"],\"rtcName\":[\"reg1\",\"reg2\",\"reg3\"],\"mRID\":[\"_2C2316E3-C3FC-4B24-BBDE-1982F4B24138\",\"_98525341-BE02-428C-B70F-6F78850F7D49\",\"_C6C04885-5480-4C12-9FAC-46D852CEDC2B\"],\"monitoredPhase\":[\"A\",\"B\",\"C\"],\"TapChanger.tculControlMode\":[\"volt\",\"volt\",\"volt\"],\"highStep\":[16,16,16],\"lowStep\":[-16,-16,-16],\"neutralStep\":[0,0,0],\"normalStep\":[0,0,0],\"TapChanger.controlEnabled\":[true,true,true],\"lineDropCompensation\":[true,true,true],\"ltcFlag\":[true,true,true],\"RegulatingControl.enabled\":[true,true,true],\"RegulatingControl.discrete\":[true,true,true],\"RegulatingControl.mode\":[\"voltage\",\"voltage\",\"voltage\"],\"step\":[10,8,11],\"targetValue\":[122.0000,122.0000,122.0000],\"targetDeadband\":[2.0000,2.0000,2.0000],\"limitVoltage\":[0.0000,0.0000,0.0000],\"stepVoltageIncrement\":[0.6250,0.6250,0.6250],\"neutralU\":[2400.0000,2400.0000,2400.0000],\"initialDelay\":[15.0000,15.0000,15.0000],\"subsequentDelay\":[2.0000,2.0000,2.0000],\"lineDropR\":[3.0000,3.0000,3.0000],\"lineDropX\":[9.0000,9.0000,9.0000],\"reverseLineDropR\":[0.0000,0.0000,0.0000],\"reverseLineDropX\":[0.0000,0.0000,0.0000],\"ctRating\":[700.0000,700.0000,700.0000],\"ctRatio\":[3500.0000,3500.0000,3500.0000],\"ptRatio\":[20.0000,20.0000,20.0000]}\n", + "#]\n", + "## {\"name\":\"RatioTapChanger_Reg\",\"mRID\":\"_2baaea4b-5f98-4d4f-8ac3-619bb2712ce7\",\"ConductingEquipment_mRID\":\"_E682BC6D-C2E2-48D0-9D08-EBD89C2767A6\",\"Terminal_mRID\":\"_339C222B-A8C7-4900-BCFE-A91B00F059C9\",\"measurementType\":\"Pos\",\"phases\":\"A\",\"MeasurementClass\":\"Discrete\",\"ConductingEquipment_type\":\"PowerTransformer\",\"ConductingEquipment_name\":\"Reg\",\"ConnectivityNode\":\"rg60\",\"SimObject\":\"reg_Reg\"},\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 181, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'name': 'cap1', 'mRID': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'CN1': '675', 'phases': 'ABC', 'kvar_A': 200.0, 'kvar_B': 200.0, 'kvar_C': 200.0, 'nominalVoltage': 4160.0, 'nomU': 4160.0, 'phaseConnection': 'Y', 'grounded': True, 'enabled': False, 'mode': None, 'targetValue': 0.0, 'targetDeadband': 0.0, 'aVRDelay': 0.0, 'monitoredName': None, 'monitoredClass': None, 'monitoredBus': None, 'monitoredPhase': None}\n", + "{'bankName': 'Reg', 'size': '3', 'bankPhases': 'ABC', 'tankName': ['reg1', 'reg2', 'reg3'], 'endNumber': [2, 2, 2], 'endPhase': ['A', 'B', 'C'], 'rtcName': ['reg1', 'reg2', 'reg3'], 'mRID': ['_6CFE3829-3905-42FD-A9AD-5C70D3D835EF', '_C03B9EC2-DD5F-4C39-A4A7-E212CBBC9940', '_5B8EA691-0051-4CF2-9A03-140171317E4D'], 'monitoredPhase': ['A', 'B', 'C'], 'TapChanger.tculControlMode': ['volt', 'volt', 'volt'], 'highStep': [16, 16, 16], 'lowStep': [-16, -16, -16], 'neutralStep': [0, 0, 0], 'normalStep': [0, 0, 0], 'TapChanger.controlEnabled': [True, True, True], 'lineDropCompensation': [True, True, True], 'ltcFlag': [True, True, True], 'RegulatingControl.enabled': [True, True, True], 'RegulatingControl.discrete': [True, True, True], 'RegulatingControl.mode': ['voltage', 'voltage', 'voltage'], 'step': [10, 8, 11], 'targetValue': [122.0, 122.0, 122.0], 'targetDeadband': [2.0, 2.0, 2.0], 'limitVoltage': [0.0, 0.0, 0.0], 'stepVoltageIncrement': [0.625, 0.625, 0.625], 'neutralU': [2400.0, 2400.0, 2400.0], 'initialDelay': [15.0, 15.0, 15.0], 'subsequentDelay': [2.0, 2.0, 2.0], 'lineDropR': [3.0, 3.0, 3.0], 'lineDropX': [9.0, 9.0, 9.0], 'reverseLineDropR': [0.0, 0.0, 0.0], 'reverseLineDropX': [0.0, 0.0, 0.0], 'ctRating': [700.0, 700.0, 700.0], 'ctRatio': [3500.0, 3500.0, 3500.0], 'ptRatio': [20.0, 20.0, 20.0]}\n", + "{'bankName': 'Reg', 'size': '3', 'bankPhases': 'ABC', 'tankName': ['reg1', 'reg2', 'reg3'], 'endNumber': [2, 2, 2], 'endPhase': ['A', 'B', 'C'], 'rtcName': ['reg1', 'reg2', 'reg3'], 'mRID': ['_6CFE3829-3905-42FD-A9AD-5C70D3D835EF', '_C03B9EC2-DD5F-4C39-A4A7-E212CBBC9940', '_5B8EA691-0051-4CF2-9A03-140171317E4D'], 'monitoredPhase': ['A', 'B', 'C'], 'TapChanger.tculControlMode': ['volt', 'volt', 'volt'], 'highStep': [16, 16, 16], 'lowStep': [-16, -16, -16], 'neutralStep': [0, 0, 0], 'normalStep': [0, 0, 0], 'TapChanger.controlEnabled': [True, True, True], 'lineDropCompensation': [True, True, True], 'ltcFlag': [True, True, True], 'RegulatingControl.enabled': [True, True, True], 'RegulatingControl.discrete': [True, True, True], 'RegulatingControl.mode': ['voltage', 'voltage', 'voltage'], 'step': [10, 8, 11], 'targetValue': [122.0, 122.0, 122.0], 'targetDeadband': [2.0, 2.0, 2.0], 'limitVoltage': [0.0, 0.0, 0.0], 'stepVoltageIncrement': [0.625, 0.625, 0.625], 'neutralU': [2400.0, 2400.0, 2400.0], 'initialDelay': [15.0, 15.0, 15.0], 'subsequentDelay': [2.0, 2.0, 2.0], 'lineDropR': [3.0, 3.0, 3.0], 'lineDropX': [9.0, 9.0, 9.0], 'reverseLineDropR': [0.0, 0.0, 0.0], 'reverseLineDropX': [0.0, 0.0, 0.0], 'ctRating': [700.0, 700.0, 700.0], 'ctRatio': [3500.0, 3500.0, 3500.0], 'ptRatio': [20.0, 20.0, 20.0]}\n", + "{'bankName': 'Reg', 'size': '3', 'bankPhases': 'ABC', 'tankName': ['reg1', 'reg2', 'reg3'], 'endNumber': [2, 2, 2], 'endPhase': ['A', 'B', 'C'], 'rtcName': ['reg1', 'reg2', 'reg3'], 'mRID': ['_6CFE3829-3905-42FD-A9AD-5C70D3D835EF', '_C03B9EC2-DD5F-4C39-A4A7-E212CBBC9940', '_5B8EA691-0051-4CF2-9A03-140171317E4D'], 'monitoredPhase': ['A', 'B', 'C'], 'TapChanger.tculControlMode': ['volt', 'volt', 'volt'], 'highStep': [16, 16, 16], 'lowStep': [-16, -16, -16], 'neutralStep': [0, 0, 0], 'normalStep': [0, 0, 0], 'TapChanger.controlEnabled': [True, True, True], 'lineDropCompensation': [True, True, True], 'ltcFlag': [True, True, True], 'RegulatingControl.enabled': [True, True, True], 'RegulatingControl.discrete': [True, True, True], 'RegulatingControl.mode': ['voltage', 'voltage', 'voltage'], 'step': [10, 8, 11], 'targetValue': [122.0, 122.0, 122.0], 'targetDeadband': [2.0, 2.0, 2.0], 'limitVoltage': [0.0, 0.0, 0.0], 'stepVoltageIncrement': [0.625, 0.625, 0.625], 'neutralU': [2400.0, 2400.0, 2400.0], 'initialDelay': [15.0, 15.0, 15.0], 'subsequentDelay': [2.0, 2.0, 2.0], 'lineDropR': [3.0, 3.0, 3.0], 'lineDropX': [9.0, 9.0, 9.0], 'reverseLineDropR': [0.0, 0.0, 0.0], 'reverseLineDropX': [0.0, 0.0, 0.0], 'ctRating': [700.0, 700.0, 700.0], 'ctRatio': [3500.0, 3500.0, 3500.0], 'ptRatio': [20.0, 20.0, 20.0]}\n" + ] + } + ], + "source": [ + "def get_device_dict(model_dict, model_line_dict, device_type, name):\n", + " if device_type == 'Shark':\n", + " for meas in model_dict['feeders'][0]['measurements']:\n", + " if meas['name'].startswith('ACLineSegment_'+name):\n", + " # print(meas)\n", + " if meas['measurementType'] == 'PNV':\n", + " if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " if meas['measurementType'] == 'VA':\n", + " if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " elif device_type == 'Beckwith CapBank 2':\n", + " #LinearShuntCompensator\n", + " for meas in model_dict['feeders'][0]['measurements']:\n", + " if meas['name'].startswith('LinearShuntCompensator_'+name):\n", + " if meas['measurementType'] == 'PNV':\n", + " if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " elif meas['measurementType'] == 'VA':\n", + " if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " if meas['measurementType'] == 'Pos':\n", + " if meas['measurementType'] not in model_line_dict[name]:\n", + " model_line_dict[name][meas['measurementType']] = {}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'pos'} \n", + " for cap in model_dict['feeders'][0][\"capacitors\"]:\n", + " if cap['name'] == name:\n", + " model_line_dict[name]['manual close'] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " print(cap)\n", + " #TODO figure this out\n", + " # 0 manual close\n", + " # 1 manual open\n", + " elif device_type == 'Beckwith LTC':\n", + " for meas in model_dict['feeders'][0]['measurements']:\n", + " if meas['name'].startswith('RatioTapChanger_'+name):#ConductingEquipment_name\n", + " if meas['measurementType'] == 'PNV':\n", + " if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " elif meas['measurementType'] == 'VA':\n", + " if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'}\n", + " if meas['measurementType'] == 'Pos':\n", + " if meas['measurementType'] not in model_line_dict[name]:\n", + " model_line_dict[name][meas['measurementType']] = {}\n", + " model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'pos'} \n", + " for reg in model_dict['feeders'][0][\"regulators\"]:\n", + " if reg['bankName'] == reg_name:\n", + " # Do I have to count for each position change?\n", + " print(reg)\n", + "import json\n", + "# names for ieee13\n", + "cap_name = \"cap1\"\n", + "reg_name = \"Reg\"\n", + "\n", + "\n", + "with open(\"model_dict.json\") as f:\n", + " model_dict = json.load(f)\n", + "\n", + "model_line_dict={line_name:{},\n", + " cap_name:{},\n", + " reg_name:{}}\n", + "line_name = \"632633\"\n", + "name=line_name\n", + "device_type = \"Shark\"\n", + "\n", + "get_device_dict(model_dict, model_line_dict, device_type, name)\n", + "device_type= \"Beckwith CapBank 2\"\n", + "name=\"cap1\"\n", + "get_device_dict(model_dict, model_line_dict, device_type, name)\n", + "device_type = 'Beckwith LTC'\n", + "name=\"Reg\"\n", + "get_device_dict(model_dict, model_line_dict, device_type, name)\n", + "\n", + "with open(\"model_line_dict.json\",\"w\") as f:\n", + " json.dump(model_line_dict,f,indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 182, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'632633': {'VA': {'C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + " 'PNV': {'C': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'type': 'angle'},\n", + " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}},\n", + " 'cap1': {'PNV': {'B': {'mrid': '_13e6fb7e-d41c-4366-a97c-f33543b4b801',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_448ef334-0751-4a27-b168-c9a9bf5d4cb4', 'type': 'angle'},\n", + " 'C': {'mrid': '_d6aabda3-6dcd-4f8e-b9d5-314c112e1bc9', 'type': 'angle'}},\n", + " 'manual close': {'mrid': '_ffe351f9-22c0-483d-8017-04dcbb6c107b',\n", + " 'type': 'magnitude'},\n", + " 'Pos': {'C': {'mrid': '_5220d83e-ee9b-451d-8ce7-2768b6ad8612',\n", + " 'type': 'pos'},\n", + " 'B': {'mrid': '_bbf78ce5-9c07-4843-b2c0-0d94305fb581', 'type': 'pos'},\n", + " 'A': {'mrid': '_ffe351f9-22c0-483d-8017-04dcbb6c107b', 'type': 'pos'}},\n", + " 'VA': {'B': {'mrid': '_6e76eb6e-363c-4d5d-b304-2e216c9451c0',\n", + " 'type': 'angle'},\n", + " 'C': {'mrid': '_9c51e7a7-3fb7-4bf2-a9f5-6c7aa4330549', 'type': 'angle'},\n", + " 'A': {'mrid': '_f6013f90-37e0-4e01-97b4-ea67cf410711', 'type': 'angle'}}},\n", + " 'Reg': {'Pos': {'A': {'mrid': '_824c02fc-18d1-4064-b313-22f9f6ce472b',\n", + " 'type': 'pos'},\n", + " 'B': {'mrid': '_c389843f-37c3-4c76-8e54-f02b70368492', 'type': 'pos'},\n", + " 'C': {'mrid': '_fea855f6-4939-4c6d-8d84-02757a82c01d', 'type': 'pos'}}}}" + ] + }, + "execution_count": 182, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_line_dict" + ] + }, + { + "cell_type": "code", + "execution_count": 183, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"conversion_dict.json\") as f:\n", + "# json.dump(shark_dict,f)\n", + " conversion_dict = json.load(f)\n", + "\n", + "with open(\"model_line_dict.json\") as f:\n", + " model_line_dict = json.load(f)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 184, + "metadata": {}, + "outputs": [], + "source": [ + "# index = 1\n", + "# values = {1:28008,2:0,3:0,4:0.11,5:0,6:0,36:200,37:0,38:0,39:0.12,40:0,41:0}\n", + "# cim_msg = {}\n", + "\n", + "# for name, device in element_to_device.items():\n", + "# model = model_line_dict[name]\n", + "# conversion = conversion_dict[device]\n", + "\n", + "# for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", + "# value = values[index]\n", + "# dnp3_attr_name = conversion['Analog input'][index]['type']\n", + "# # print(meas_type,model[meas_type])\n", + "# if model[dnp3_attr_name]['mrid'] not in cim_msg:\n", + "# cim_msg[model[dnp3_attr_name]['mrid']] = {'magnitude':0,'angle':0}\n", + "# value_type = model[dnp3_attr_name]['type']\n", + "# cim_msg[model[dnp3_attr_name]['mrid']][value_type] = value # time multipier\n", + "# # for type_name, mrid in model.items():\n", + "# # print(conversion[index],type_name)\n", + "# cim_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 185, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'VA': {'C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + " 'PNV': {'C': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'type': 'angle'},\n", + " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}}" + ] + }, + "execution_count": 185, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model" + ] + }, + { + "cell_type": "code", + "execution_count": 186, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n" + ] + }, + { + "cell_type": "code", + "execution_count": 187, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'exclusion_dict' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexclusion_dict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_csv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"exclusion.csv\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'exclusion_dict' is not defined" + ] + } + ], + "source": [ + "pd.DataFrame.from_dict(exclusion_dict).to_csv(\"exclusion.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": 188, + "metadata": {}, + "outputs": [], + "source": [ + "def get_conversion_model(csv_file,sheet_name):\n", + " df = pd.read_excel(csv_file,sheet_name=sheet_name)\n", + " master_dict ={\n", + " 'Analog input':{},\n", + " 'Analog output':{},\n", + " 'Binary input':{},\n", + " 'Binary output':{} }\n", + "\n", + "# temp_df = pd.DataFrame(df[1:4])\n", + "# temp_df = temp_df.set_index('Index')\n", + "# master_dict['Analog input'] = temp_df.T\n", + "\n", + "# temp_df = pd.DataFrame(df[6:10])\n", + "# temp_df = temp_df.set_index('Index')\n", + "# master_dict['Analog output'] = temp_df.T\n", + "\n", + " x = []\n", + "\n", + " for row in df.iterrows():\n", + " if pd.isna(row[1][2]):\n", + " # print(row[0])\n", + " x.append(row[0])\n", + "# print(df.shape)\n", + " x.append(df.shape[0])\n", + " it = iter(x)\n", + " for x in it:\n", + " type_name = df.iloc[x][1]\n", + " print(type_name)\n", + " next_value = next(it)\n", + " print (x+1, next_value)\n", + " print(pd.DataFrame(df[x+1:next_value]))\n", + " temp_df = pd.DataFrame(df[x+1: next_value])\n", + " temp_df = temp_df.set_index('Index')\n", + " master_dict[type_name] = temp_df.T.to_dict()\n", + " return master_dict\n" + ] + }, + { + "cell_type": "code", + "execution_count": 189, + "metadata": {}, + "outputs": [], + "source": [ + "csv_file = 'DNP3 list.xlsx'" + ] + }, + { + "cell_type": "code", + "execution_count": 190, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analog input\n", + "1 21\n", + " Index type Multiplier MultiplierString Units \\\n", + "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", + "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", + "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", + "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", + "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V \n", + "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V \n", + "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A \n", + "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A \n", + "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A \n", + "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W \n", + "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", + "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", + "13 13.0 PF 0.001000 0.001 NaN \n", + "14 13.0 Hz 0.010000 0.01 Hz \n", + "15 36.0 P A 0.137329 (4500/32768) W \n", + "16 37.0 P B 0.137329 (4500/32768) W \n", + "17 38.0 P C 0.137329 (4500/32768) W \n", + "18 39.0 Q A 0.137329 (4500/32768) VARs \n", + "19 40.0 Q B 0.137329 (4500/32768) VARs \n", + "20 41.0 Q C 0.137329 (4500/32768) VARs \n", + "\n", + " CIM attribute CIM phase CIM units \n", + "1 magnitude A PNV \n", + "2 magnitude B PNV \n", + "3 magnitude C PNV \n", + "4 angle A PNV \n", + "5 angle B PNV \n", + "6 angle C PNV \n", + "7 x A PNV \n", + "8 y B PNV \n", + "9 z C PNV \n", + "10 NaN NaN NaN \n", + "11 NaN NaN NaN \n", + "12 NaN NaN NaN \n", + "13 NaN NaN NaN \n", + "14 NaN NaN NaN \n", + "15 real A VA \n", + "16 real B VA \n", + "17 real C VA \n", + "18 imag A VA \n", + "19 imag B VA \n", + "20 iamg C VA \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jsimpson/.conda/envs/adms36/lib/python3.6/site-packages/ipykernel_launcher.py:34: UserWarning: DataFrame columns are not unique, some columns will be omitted.\n" + ] + } + ], + "source": [ + "shark = get_conversion_model(csv_file, sheet_name='Shark')\n", + "conversion_dict['Shark'] = shark" + ] + }, + { + "cell_type": "code", + "execution_count": 191, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{1.0: {'type': 'Voltage feedback A (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", + " 2.0: {'type': 'Voltage feedback B (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", + " 3.0: {'type': 'Voltage feedback C (L-N)',\n", + " 'Multiplier': 0.00457763671,\n", + " 'MultiplierString': '(150 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'magnitude',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", + " 4.0: {'type': 'Voltage feedback A (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", + " 5.0: {'type': 'Voltage feedback B (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", + " 6.0: {'type': 'Voltage feedback C (L-L)',\n", + " 'Multiplier': 0.00915527343,\n", + " 'MultiplierString': '(300 / 32768) ',\n", + " 'Units': 'V',\n", + " 'CIM attribute': 'angle',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", + " 7.0: {'type': 'Current feedback (A)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", + " 'Units': 'A',\n", + " 'CIM attribute': 'x',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'PNV'},\n", + " 8.0: {'type': 'Current feedback (B)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", + " 'Units': 'A',\n", + " 'CIM attribute': 'y',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'PNV'},\n", + " 9.0: {'type': 'Current feedback (C)',\n", + " 'Multiplier': 0.00030517578,\n", + " 'MultiplierString': '(10 / 32768) ',\n", + " 'Units': 'A',\n", + " 'CIM attribute': 'z',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'PNV'},\n", + " 10.0: {'type': 'Total 3 ph P',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", + " 'Units': 'W',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 11.0: {'type': 'Total 3 ph Q',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 12.0: {'type': 'Total 3 ph VA',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500 / 32768) ',\n", + " 'Units': 'VA',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 13.0: {'type': 'Hz',\n", + " 'Multiplier': 0.01,\n", + " 'MultiplierString': 0.01,\n", + " 'Units': 'Hz',\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan},\n", + " 36.0: {'type': 'P A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'VA'},\n", + " 37.0: {'type': 'P B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'VA'},\n", + " 38.0: {'type': 'P C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'W',\n", + " 'CIM attribute': 'real',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'VA'},\n", + " 39.0: {'type': 'Q A',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'VA'},\n", + " 40.0: {'type': 'Q B',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'imag',\n", + " 'CIM phase': 'B',\n", + " 'CIM units': 'VA'},\n", + " 41.0: {'type': 'Q C',\n", + " 'Multiplier': 0.13732910156,\n", + " 'MultiplierString': '(4500/32768)',\n", + " 'Units': 'VARs',\n", + " 'CIM attribute': 'iamg',\n", + " 'CIM phase': 'C',\n", + " 'CIM units': 'VA'}}" + ] + }, + "execution_count": 191, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "shark['Analog input']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# 0\tmanual close\t1\tBinary\t\t\t\t\t\n", + "# 1\tmanual open\t1\tBinary\t\t\t\t\t\n", + "# { \n", + "# \"command\":\"update\",\n", + "# \"input\":{ \n", + "# \"simulation_id\":\"384642907\",\n", + "# \"message\":{ \n", + "# \"timestamp\":1573787950,\n", + "# \"difference_mrid\":\"96aa6439-d42e-4d72-a1d7-925373927c78\",\n", + "# \"reverse_differences\":[ \n", + "# { \n", + "# \"object\":\"_C1CF0980-10AF-49A5-BD61-F46CC1D92365\",\n", + "# \"attribute\":\"Switch.open\",\n", + "# \"value\":0\n", + "# }\n", + "# ],\n", + "# \"forward_differences\":[ \n", + "# { \n", + "# \"object\":\"_C1CF0980-10AF-49A5-BD61-F46CC1D92365\",\n", + "# \"attribute\":\"Switch.open\",\n", + "# \"value\":1\n", + "# }\n", + "# ]\n", + "# }\n", + "# }\n", + "# }\n", + "control_conversion_dict = {\n", + " 'object' ='_C1CF0980-10AF-49A5-BD61-F46CC1D92365',\n", + " \"attribute\":\"Switch.open\",\n", + " \"value\":1, # use index 1 (double check if thise is open) if \n", + " \"value_dict\":{'open_index':1, 'close_index':0}\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 192, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analog input\n", + "1 4\n", + " Index Type Multiplier Units CIM attribute \\\n", + "1 0.0 Voltage feedback (secondary) 0.1 V NaN \n", + "2 11111.0 Current feedback (Secondary) 1.0 V NaN \n", + "3 3.0 Primary VArs 1.0 NaN NaN \n", + "\n", + " CIM phase CIM units Group Variation \n", + "1 NaN NaN NaN NaN \n", + "2 NaN NaN NaN NaN \n", + "3 NaN NaN NaN NaN \n", + "Analog output\n", + "6 10\n", + " Index Type Multiplier Units CIM attribute CIM phase \\\n", + "6 7.0 Overvoltage Limit 1.0 NaN NaN NaN \n", + "7 8.0 Undervoltage limit 1.0 NaN NaN NaN \n", + "8 11111.0 Close delay time 1.0 NaN NaN NaN \n", + "9 11112.0 open delay time 1.0 NaN NaN NaN \n", + "\n", + " CIM units Group Variation \n", + "6 NaN NaN NaN \n", + "7 NaN NaN NaN \n", + "8 NaN NaN NaN \n", + "9 NaN NaN NaN \n", + "Binary output\n", + "12 15\n", + " Index Type Multiplier Units CIM attribute CIM phase \\\n", + "12 0.0 manual close 1.0 Binary NaN NaN \n", + "13 1.0 manual open 1.0 Binary NaN NaN \n", + "14 11111.0 Scada Control 1.0 Binary NaN NaN \n", + "\n", + " CIM units Group Variation \n", + "12 NaN NaN NaN \n", + "13 NaN NaN NaN \n", + "14 NaN NaN NaN \n", + "Binary input\n", + "17 24\n", + " Index Type Multiplier Units CIM attribute CIM phase \\\n", + "17 11111.0 Enabled 1.0 Binary NaN NaN \n", + "18 6.0 Remote 1.0 Binary NaN NaN \n", + "19 7.0 Local 1.0 Binary NaN NaN \n", + "20 4.0 Manual 1.0 Binary NaN NaN \n", + "21 5.0 Auto 1.0 Binary NaN NaN \n", + "22 0.0 Close status 1.0 Binary Pos ABC \n", + "23 1.0 Open Status 1.0 Binary Pos ABC \n", + "\n", + " CIM units Group Variation \n", + "17 NaN NaN NaN \n", + "18 NaN NaN NaN \n", + "19 NaN NaN NaN \n", + "20 NaN NaN NaN \n", + "21 NaN NaN NaN \n", + "22 Pos NaN NaN \n", + "23 Pos NaN NaN \n", + "Analog input\n", + "1 4\n", + " Index Type Multiplier CIM attribute \\\n", + "1 0.0 Load Voltage feedback (secondary) 1.0 NaN \n", + "2 2.0 Load Current feedback (Secondary) 1.0 NaN \n", + "3 5.0 Tap position 1.0 Pos \n", + "\n", + " CIM phase CIM units Units Group Variation \n", + "1 NaN NaN NaN NaN NaN \n", + "2 NaN NaN NaN NaN NaN \n", + "3 A Pos NaN NaN NaN \n", + "Analog output:\n", + "6 13\n", + " Index Type Multiplier CIM attribute \\\n", + "6 0.0 Fail safe Voltage set point 1.0 NaN \n", + "7 2.0 Fail Safe Voltage bandwidth (dead band) 1.0 NaN \n", + "8 34.0 Emergency Voltage set point 1.0 NaN \n", + "9 236.0 Emergency Voltage bandwidth  (dead band) 1.0 NaN \n", + "10 15.0 Voltage max 1.0 NaN \n", + "11 14.0 voltage min 1.0 NaN \n", + "12 4.0 Time Delay 1.0 NaN \n", + "\n", + " CIM phase CIM units Units Group Variation \n", + "6 NaN NaN NaN NaN NaN \n", + "7 NaN NaN NaN NaN NaN \n", + "8 NaN NaN NaN NaN NaN \n", + "9 NaN NaN NaN NaN NaN \n", + "10 NaN NaN NaN NaN NaN \n", + "11 NaN NaN NaN NaN NaN \n", + "12 NaN NaN NaN NaN NaN \n", + "Binary output:\n", + "15 19\n", + " Index Type Multiplier CIM attribute CIM phase CIM units \\\n", + "15 0.0 manual raise 1 Tap 1.0 NaN NaN NaN \n", + "16 1.0 manual Lower 1 Tap 1.0 NaN NaN NaN \n", + "17 5.0 Scada Control 1.0 NaN NaN NaN \n", + "18 277.0 Enabled 1.0 NaN NaN NaN \n", + "\n", + " Units Group Variation \n", + "15 NaN NaN NaN \n", + "16 NaN NaN NaN \n", + "17 NaN NaN NaN \n", + "18 NaN NaN NaN \n", + "Binary input:\n", + "21 24\n", + " Index Type Multiplier CIM attribute CIM phase CIM units Units \\\n", + "21 11.0 In band 1.0 NaN NaN NaN NaN \n", + "22 27.0 Remote/Local 1.0 NaN NaN NaN NaN \n", + "23 37.0 Auto/Manual 1.0 NaN NaN NaN NaN \n", + "\n", + " Group Variation \n", + "21 NaN NaN \n", + "22 NaN NaN \n", + "23 NaN NaN \n" + ] + } + ], + "source": [ + "sheet_name='Beckwith CapBank 2'\n", + "beckwith_capbank = get_conversion_model(csv_file,sheet_name)\n", + "conversion_dict[sheet_name]= beckwith_capbank\n", + "sheet_name ='Beckwith LTC'\n", + "beckwith_capbank = get_conversion_model(csv_file,sheet_name)\n", + "conversion_dict[sheet_name]= beckwith_capbank\n", + "with open(\"conversion_dict.json\",\"w\") as f:\n", + " json.dump(conversion_dict,f,indent=2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 193, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
typeMultiplierMultiplierStringUnitsCIM attributeCIM phaseCIM units
Index
1.0Voltage feedback A (L-N)0.004578(150 / 32768)VmagnitudeAPNV
2.0Voltage feedback B (L-N)0.004578(150 / 32768)VmagnitudeBPNV
3.0Voltage feedback C (L-N)0.004578(150 / 32768)VmagnitudeCPNV
4.0Voltage feedback A (L-L)0.009155(300 / 32768)VangleAPNV
\n", + "
" + ], + "text/plain": [ + " type Multiplier MultiplierString Units \\\n", + "Index \n", + "1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", + "2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", + "3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", + "4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", + "\n", + " CIM attribute CIM phase CIM units \n", + "Index \n", + "1.0 magnitude A PNV \n", + "2.0 magnitude B PNV \n", + "3.0 magnitude C PNV \n", + "4.0 angle A PNV " + ] + }, + "execution_count": 193, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pd.DataFrame(df[1:4])" + ] + }, + { + "cell_type": "code", + "execution_count": 194, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{0.0: {'Type': 'Load Voltage feedback (secondary)',\n", + " 'Multiplier': 1.0,\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan,\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan},\n", + " 2.0: {'Type': 'Load Current feedback\\xa0(Secondary)',\n", + " 'Multiplier': 1.0,\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan,\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan},\n", + " 5.0: {'Type': 'Tap position',\n", + " 'Multiplier': 1.0,\n", + " 'CIM attribute': 'Pos',\n", + " 'CIM phase': 'A',\n", + " 'CIM units': 'Pos',\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan}}" + ] + }, + "execution_count": 194, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beckwith_capbank['Analog input']" + ] + }, + { + "cell_type": "code", + "execution_count": 195, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'Type': 'Load Voltage feedback (secondary)',\n", + " 'Multiplier': 1.0,\n", + " 'CIM attribute': nan,\n", + " 'CIM phase': nan,\n", + " 'CIM units': nan,\n", + " 'Units': nan,\n", + " 'Group': nan,\n", + " 'Variation': nan}" + ] + }, + "execution_count": 195, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "beckwith_capbank['Analog input'][0]" + ] + }, + { + "cell_type": "code", + "execution_count": 196, + "metadata": {}, + "outputs": [], + "source": [ + "element_to_device = {'632633':'Shark',\n", + " 'cap1':'Beckwith CapBank 2',\n", + " 'Reg':'Beckwith LTC'}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 197, + "metadata": {}, + "outputs": [], + "source": [ + "def update_cim_msg_analog(CIM_msg,index,value,conversion,model):\n", + " if 'Analog input' in conversion and index in conversion['Analog input']:\n", + " CIM_phase = conversion['Analog input'][index]['CIM phase']\n", + " CIM_units = conversion['Analog input'][index]['CIM units']\n", + " CIM_attribute = conversion['Analog input'][index]['CIM attribute']\n", + " ## Check if multiplier is na or str\n", + " multiplier = conversion['Analog input'][index]['Multiplier']\n", + " mrid = model[CIM_units][CIM_phase]['mrid']\n", + " if type(multiplier) == str:\n", + " multiplier = 1\n", + "\n", + " CIM_value = {'mrid':mrid} \n", + " if CIM_units == 'PNV' or CIM_units == 'VA':\n", + " CIM_value = {'mrid':mrid, 'magnitude':0,'angle':0} \n", + "\n", + " if mrid not in CIM_msg:\n", + " CIM_msg[mrid] = CIM_value \n", + " CIM_msg[mrid][CIM_attribute] = value * multiplier # times multipier\n", + "def update_cim_msg_binary(CIM_msg,index,value,conversion,model):\n", + "# print(conversion['Binary input'][index])\n", + " if 'Binary input' in conversion and index in conversion['Binary input']:\n", + " CIM_phases = conversion['Binary input'][index]['CIM phase']\n", + " CIM_units = conversion['Binary input'][index]['CIM units']\n", + " CIM_attribute = conversion['Binary input'][index]['CIM attribute']\n", + " ## Check if multiplier is na or str\n", + " multiplier = conversion['Binary input'][index]['Multiplier']\n", + " for CIM_phase in CIM_phases:\n", + " mrid = model[CIM_units][CIM_phase]['mrid']\n", + " CIM_value = {'mrid':mrid} \n", + " if mrid not in CIM_msg:\n", + " CIM_msg[mrid] = CIM_value \n", + " CIM_msg[mrid][CIM_attribute] = value * multiplier # times multipier" + ] + }, + { + "cell_type": "code", + "execution_count": 198, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_ffe351f9-22c0-483d-8017-04dcbb6c107b': {'mrid': '_ffe351f9-22c0-483d-8017-04dcbb6c107b',\n", + " 'Pos': 12.0}}" + ] + }, + "execution_count": 198, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Test LTC\n", + "element_to_device = {'cap1':'Beckwith LTC'}\n", + "# index 5, tap pos 12\n", + "values = {5:12}\n", + "CIM_msg = {}\n", + "for name, device in element_to_device.items():\n", + " model = model_line_dict[name]\n", + " conversion = conversion_dict[device]\n", + " for index in [5]:\n", + " value = values[index]\n", + " update_cim_msg_analog(CIM_msg,index,value,conversion,model)\n", + "\n", + "CIM_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 199, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_ffe351f9-22c0-483d-8017-04dcbb6c107b': {'mrid': '_ffe351f9-22c0-483d-8017-04dcbb6c107b',\n", + " 'Pos': 1.0},\n", + " '_bbf78ce5-9c07-4843-b2c0-0d94305fb581': {'mrid': '_bbf78ce5-9c07-4843-b2c0-0d94305fb581',\n", + " 'Pos': 1.0},\n", + " '_5220d83e-ee9b-451d-8ce7-2768b6ad8612': {'mrid': '_5220d83e-ee9b-451d-8ce7-2768b6ad8612',\n", + " 'Pos': 1.0}}" + ] + }, + "execution_count": 199, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Test capbank\n", + "element_to_device = {'cap1':'Beckwith CapBank 2'}\n", + "values = {0:0,1:1}\n", + "CIM_msg = {}\n", + "for name, device in element_to_device.items():\n", + " model = model_line_dict[name]\n", + " conversion = conversion_dict[device]\n", + "\n", + " for index in [0,1]:\n", + " value = values[index]\n", + " update_cim_msg_binary(CIM_msg,index,value,conversion,model)\n", + "\n", + "CIM_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 200, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'_806d5de7-541b-4944-8825-c7afffa5abc2': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2',\n", + " 'magnitude': 128.21044897368,\n", + " 'angle': 0.0010070800773},\n", + " '_bd5f3945-5d16-4694-896e-92da8a0e4895': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895',\n", + " 'magnitude': 0.0,\n", + " 'angle': 0.0},\n", + " '_5bc367a5-f771-494a-83bd-b4560d538df7': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'magnitude': 0.0,\n", + " 'angle': 0.0},\n", + " '_4044e33a-ecc2-4d3d-8584-f65643c4eea2': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2',\n", + " 'magnitude': 0,\n", + " 'angle': 0,\n", + " 'real': 27.465820311999998,\n", + " 'imag': 0.0164794921872},\n", + " '_ebd287b8-bceb-4471-a709-c82fbd34bd30': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30',\n", + " 'magnitude': 0,\n", + " 'angle': 0,\n", + " 'real': 0.0,\n", + " 'imag': 0.0},\n", + " '_11bf885c-b677-42ba-859c-d1a013a9f36e': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'magnitude': 0,\n", + " 'angle': 0,\n", + " 'real': 0.0,\n", + " 'iamg': 0.0}}" + ] + }, + "execution_count": 200, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "## Test shark\n", + "values = {1:28008,2:0,3:0,4:0.11,5:0,6:0,36:200,37:0,38:0,39:0.12,40:0,41:0}\n", + "CIM_msg = {}\n", + "element_to_device = {'632633':'Shark'}\n", + "for name, device in element_to_device.items():\n", + " model = model_line_dict[name]\n", + " conversion = conversion_dict[device]\n", + "\n", + " for index in [1,2,3,4,5,6,36,37,38,39,40,41]:\n", + " value = values[index]\n", + " update_cim_msg_analog(CIM_msg,index,value,conversion,model)\n", + " update_cim_msg_binary(CIM_msg,index,value,conversion,model)\n", + "\n", + "CIM_msg" + ] + }, + { + "cell_type": "code", + "execution_count": 201, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'VA': {'C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", + " 'B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", + " 'PNV': {'C': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'type': 'angle'},\n", + " 'A': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'type': 'angle'},\n", + " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}}" + ] + }, + "execution_count": 201, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model" + ] + }, + { + "cell_type": "code", + "execution_count": 202, + "metadata": {}, + "outputs": [], + "source": [ + "cim_phase = conversion['Analog input'][index]['CIM phase']\n", + "cim_units = conversion['Analog input'][index]['CIM units']\n", + "cim_attribute = conversion['Analog input'][index]['CIM attribute']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From ef3c07e8a99276501b0627ccb7e58bfe9539b699 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 13 Jan 2021 09:33:19 -0700 Subject: [PATCH 35/64] Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- dnp3/service/dnp3/dnp3_to_cim.py | 23 +- dnp3/service/dnp3/master.py | 10 + test/Mapping_CIM_Attribute.ipynb | 904 ++++++++++++++++++------------- test/master_main.py | 3 +- 4 files changed, 537 insertions(+), 403 deletions(-) diff --git a/dnp3/service/dnp3/dnp3_to_cim.py b/dnp3/service/dnp3/dnp3_to_cim.py index 09909d6..4eb66ac 100644 --- a/dnp3/service/dnp3/dnp3_to_cim.py +++ b/dnp3/service/dnp3/dnp3_to_cim.py @@ -1,19 +1,22 @@ import json import pandas as pd +import numpy as np def get_conversion_model(csv_file,sheet_name): df = pd.read_excel(csv_file,sheet_name=sheet_name) - master_dict ={ - 'Analog input':{}, - 'Analog output':{}, - 'Binary input':{}, - 'Binary output':{} + df = df.replace(np.nan, '', regex=True) + master_dict = { + 'Analog input': {}, + 'Analog output': {}, + 'Binary input': {}, + 'Binary output': {} } x = [] - for row in df.iterrows(): - if pd.isna(row[1][2]): - # print(row[0]) - x.append(row[0]) + ## Check for row breaks where there is no value or NaN for the 'Multipler' + for index, row in df.iterrows(): + if pd.isna(row['Multiplier']) or row['Multiplier'] == '': + #print(index) + x.append(index) # print(df.shape) x.append(df.shape[0]) it = iter(x) @@ -21,7 +24,7 @@ def get_conversion_model(csv_file,sheet_name): type_name = df.iloc[x][1] print(type_name) next_value = next(it) - print (x+1, next_value) + print(x+1, next_value) print(pd.DataFrame(df[x+1:next_value])) temp_df = pd.DataFrame(df[x+1: next_value]) temp_df = temp_df.set_index('Index') diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index b48399f..f81a328 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -281,6 +281,10 @@ def update_cim_msg_analog(self, CIM_msg, index, value, conversion, model): CIM_attribute = conversion['Analog input'][index]['CIM attribute'] ## Check if multiplier is na or str multiplier = conversion['Analog input'][index]['Multiplier'] + if CIM_units not in model: + print(str(CIM_units) +' not in model') + return + mrid = model[CIM_units][CIM_phase]['mrid'] if type(multiplier) == str: multiplier = 1 @@ -301,7 +305,13 @@ def update_cim_msg_binary(self, CIM_msg, index, value, conversion,model): CIM_attribute = conversion['Binary input'][index]['CIM attribute'] ## Check if multiplier is na or str multiplier = conversion['Binary input'][index]['Multiplier'] + print(type(CIM_units),CIM_units) + if CIM_units not in model: + print(str(CIM_units) +' not in model') + return for CIM_phase in CIM_phases: + # print(model) + # exit(0) mrid = model[CIM_units][CIM_phase]['mrid'] CIM_value = {'mrid': mrid} if mrid not in CIM_msg: diff --git a/test/Mapping_CIM_Attribute.ipynb b/test/Mapping_CIM_Attribute.ipynb index 499d288..2963698 100644 --- a/test/Mapping_CIM_Attribute.ipynb +++ b/test/Mapping_CIM_Attribute.ipynb @@ -2,17 +2,18 @@ "cells": [ { "cell_type": "code", - "execution_count": 67, + "execution_count": 232, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", - "import json\n" + "import json\n", + "import numpy as np\n" ] }, { "cell_type": "code", - "execution_count": 176, + "execution_count": 234, "metadata": {}, "outputs": [ { @@ -49,20 +50,20 @@ " \n", " \n", " 0\n", - " NaN\n", + " \n", " Analog input\n", - " NaN\n", - " NaN\n", - " NaN\n", - " NaN\n", - " NaN\n", - " NaN\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " 1\n", - " 1.0\n", + " 1\n", " Voltage feedback A (L-N)\n", - " 0.004578\n", + " 0.00457764\n", " (150 / 32768)\n", " V\n", " magnitude\n", @@ -71,9 +72,9 @@ " \n", " \n", " 2\n", - " 2.0\n", + " 2\n", " Voltage feedback B (L-N)\n", - " 0.004578\n", + " 0.00457764\n", " (150 / 32768)\n", " V\n", " magnitude\n", @@ -82,9 +83,9 @@ " \n", " \n", " 3\n", - " 3.0\n", + " 3\n", " Voltage feedback C (L-N)\n", - " 0.004578\n", + " 0.00457764\n", " (150 / 32768)\n", " V\n", " magnitude\n", @@ -93,9 +94,9 @@ " \n", " \n", " 4\n", - " 4.0\n", + " 4\n", " Voltage feedback A (L-L)\n", - " 0.009155\n", + " 0.00915527\n", " (300 / 32768)\n", " V\n", " angle\n", @@ -104,9 +105,9 @@ " \n", " \n", " 5\n", - " 5.0\n", + " 5\n", " Voltage feedback B (L-L)\n", - " 0.009155\n", + " 0.00915527\n", " (300 / 32768)\n", " V\n", " angle\n", @@ -115,9 +116,9 @@ " \n", " \n", " 6\n", - " 6.0\n", + " 6\n", " Voltage feedback C (L-L)\n", - " 0.009155\n", + " 0.00915527\n", " (300 / 32768)\n", " V\n", " angle\n", @@ -126,9 +127,9 @@ " \n", " \n", " 7\n", - " 7.0\n", + " 7\n", " Current feedback (A)\n", - " 0.000305\n", + " 0.000305176\n", " (10 / 32768)\n", " A\n", " x\n", @@ -137,9 +138,9 @@ " \n", " \n", " 8\n", - " 8.0\n", + " 8\n", " Current feedback (B)\n", - " 0.000305\n", + " 0.000305176\n", " (10 / 32768)\n", " A\n", " y\n", @@ -148,9 +149,9 @@ " \n", " \n", " 9\n", - " 9.0\n", + " 9\n", " Current feedback (C)\n", - " 0.000305\n", + " 0.000305176\n", " (10 / 32768)\n", " A\n", " z\n", @@ -159,62 +160,62 @@ " \n", " \n", " 10\n", - " 10.0\n", + " 10\n", " Total 3 ph P\n", " 0.137329\n", " (4500 / 32768)\n", " W\n", - " NaN\n", - " NaN\n", - " NaN\n", + " \n", + " \n", + " \n", " \n", " \n", " 11\n", - " 11.0\n", + " 11\n", " Total 3 ph Q\n", " 0.137329\n", " (4500 / 32768)\n", " VARs\n", - " NaN\n", - " NaN\n", - " NaN\n", + " \n", + " \n", + " \n", " \n", " \n", " 12\n", - " 12.0\n", + " 12\n", " Total 3 ph VA\n", " 0.137329\n", " (4500 / 32768)\n", " VA\n", - " NaN\n", - " NaN\n", - " NaN\n", + " \n", + " \n", + " \n", " \n", " \n", " 13\n", - " 13.0\n", + " 13\n", " PF\n", - " 0.001000\n", " 0.001\n", - " NaN\n", - " NaN\n", - " NaN\n", - " NaN\n", + " 0.001\n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", " 14\n", - " 13.0\n", + " 13\n", " Hz\n", - " 0.010000\n", + " 0.01\n", " 0.01\n", " Hz\n", - " NaN\n", - " NaN\n", - " NaN\n", + " \n", + " \n", + " \n", " \n", " \n", " 15\n", - " 36.0\n", + " 36\n", " P A\n", " 0.137329\n", " (4500/32768)\n", @@ -225,7 +226,7 @@ " \n", " \n", " 16\n", - " 37.0\n", + " 37\n", " P B\n", " 0.137329\n", " (4500/32768)\n", @@ -236,7 +237,7 @@ " \n", " \n", " 17\n", - " 38.0\n", + " 38\n", " P C\n", " 0.137329\n", " (4500/32768)\n", @@ -247,7 +248,7 @@ " \n", " \n", " 18\n", - " 39.0\n", + " 39\n", " Q A\n", " 0.137329\n", " (4500/32768)\n", @@ -258,7 +259,7 @@ " \n", " \n", " 19\n", - " 40.0\n", + " 40\n", " Q B\n", " 0.137329\n", " (4500/32768)\n", @@ -269,7 +270,7 @@ " \n", " \n", " 20\n", - " 41.0\n", + " 41\n", " Q C\n", " 0.137329\n", " (4500/32768)\n", @@ -283,31 +284,31 @@ "" ], "text/plain": [ - " Index type Multiplier MultiplierString Units \\\n", - "0 NaN Analog input NaN NaN NaN \n", - "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", - "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", - "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", - "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", - "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V \n", - "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V \n", - "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A \n", - "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A \n", - "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A \n", - "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W \n", - "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", - "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", - "13 13.0 PF 0.001000 0.001 NaN \n", - "14 13.0 Hz 0.010000 0.01 Hz \n", - "15 36.0 P A 0.137329 (4500/32768) W \n", - "16 37.0 P B 0.137329 (4500/32768) W \n", - "17 38.0 P C 0.137329 (4500/32768) W \n", - "18 39.0 Q A 0.137329 (4500/32768) VARs \n", - "19 40.0 Q B 0.137329 (4500/32768) VARs \n", - "20 41.0 Q C 0.137329 (4500/32768) VARs \n", + " Index type Multiplier MultiplierString Units \\\n", + "0 Analog input \n", + "1 1 Voltage feedback A (L-N) 0.00457764 (150 / 32768) V \n", + "2 2 Voltage feedback B (L-N) 0.00457764 (150 / 32768) V \n", + "3 3 Voltage feedback C (L-N) 0.00457764 (150 / 32768) V \n", + "4 4 Voltage feedback A (L-L) 0.00915527 (300 / 32768) V \n", + "5 5 Voltage feedback B (L-L) 0.00915527 (300 / 32768) V \n", + "6 6 Voltage feedback C (L-L) 0.00915527 (300 / 32768) V \n", + "7 7 Current feedback (A) 0.000305176 (10 / 32768) A \n", + "8 8 Current feedback (B) 0.000305176 (10 / 32768) A \n", + "9 9 Current feedback (C) 0.000305176 (10 / 32768) A \n", + "10 10 Total 3 ph P 0.137329 (4500 / 32768) W \n", + "11 11 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", + "12 12 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", + "13 13 PF 0.001 0.001 \n", + "14 13 Hz 0.01 0.01 Hz \n", + "15 36 P A 0.137329 (4500/32768) W \n", + "16 37 P B 0.137329 (4500/32768) W \n", + "17 38 P C 0.137329 (4500/32768) W \n", + "18 39 Q A 0.137329 (4500/32768) VARs \n", + "19 40 Q B 0.137329 (4500/32768) VARs \n", + "20 41 Q C 0.137329 (4500/32768) VARs \n", "\n", " CIM attribute CIM phase CIM units \n", - "0 NaN NaN NaN \n", + "0 \n", "1 magnitude A PNV \n", "2 magnitude B PNV \n", "3 magnitude C PNV \n", @@ -317,11 +318,11 @@ "7 x A PNV \n", "8 y B PNV \n", "9 z C PNV \n", - "10 NaN NaN NaN \n", - "11 NaN NaN NaN \n", - "12 NaN NaN NaN \n", - "13 NaN NaN NaN \n", - "14 NaN NaN NaN \n", + "10 \n", + "11 \n", + "12 \n", + "13 \n", + "14 \n", "15 real A VA \n", "16 real B VA \n", "17 real C VA \n", @@ -330,19 +331,20 @@ "20 iamg C VA " ] }, - "execution_count": 176, + "execution_count": 234, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df = pd.read_excel (r'DNP3 list.xlsx', sheet_name='Shark')\n", + "df = df.replace(np.nan, '', regex=True)\n", "df" ] }, { "cell_type": "code", - "execution_count": 177, + "execution_count": 235, "metadata": {}, "outputs": [], "source": [ @@ -352,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 178, + "execution_count": 236, "metadata": {}, "outputs": [ { @@ -366,13 +368,13 @@ { "data": { "text/plain": [ - "{nan: {'type': 'Analog input',\n", - " 'Multiplier': nan,\n", - " 'MultiplierString': nan,\n", - " 'Units': nan,\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", + "{'': {'type': 'Analog input',\n", + " 'Multiplier': '',\n", + " 'MultiplierString': '',\n", + " 'Units': '',\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", " 1.0: {'type': 'Voltage feedback A (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", @@ -440,30 +442,30 @@ " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'W',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", " 11.0: {'type': 'Total 3 ph Q',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VARs',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", " 12.0: {'type': 'Total 3 ph VA',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VA',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", " 13.0: {'type': 'Hz',\n", " 'Multiplier': 0.01,\n", " 'MultiplierString': 0.01,\n", " 'Units': 'Hz',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", " 36.0: {'type': 'P A',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", @@ -508,7 +510,7 @@ " 'CIM units': 'VA'}}" ] }, - "execution_count": 178, + "execution_count": 236, "metadata": {}, "output_type": "execute_result" } @@ -521,7 +523,7 @@ }, { "cell_type": "code", - "execution_count": 179, + "execution_count": 237, "metadata": {}, "outputs": [], "source": [ @@ -532,7 +534,7 @@ }, { "cell_type": "code", - "execution_count": 180, + "execution_count": 238, "metadata": {}, "outputs": [], "source": [ @@ -551,7 +553,7 @@ }, { "cell_type": "code", - "execution_count": 181, + "execution_count": 239, "metadata": {}, "outputs": [ { @@ -659,7 +661,7 @@ }, { "cell_type": "code", - "execution_count": 182, + "execution_count": 240, "metadata": {}, "outputs": [ { @@ -693,7 +695,7 @@ " 'C': {'mrid': '_fea855f6-4939-4c6d-8d84-02757a82c01d', 'type': 'pos'}}}}" ] }, - "execution_count": 182, + "execution_count": 240, "metadata": {}, "output_type": "execute_result" } @@ -704,7 +706,7 @@ }, { "cell_type": "code", - "execution_count": 183, + "execution_count": 241, "metadata": {}, "outputs": [], "source": [ @@ -718,7 +720,7 @@ }, { "cell_type": "code", - "execution_count": 184, + "execution_count": 242, "metadata": {}, "outputs": [], "source": [ @@ -745,23 +747,28 @@ }, { "cell_type": "code", - "execution_count": 185, + "execution_count": 243, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'VA': {'C': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", + "{'PNV': {'B': {'mrid': '_13e6fb7e-d41c-4366-a97c-f33543b4b801',\n", " 'type': 'angle'},\n", - " 'A': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2', 'type': 'angle'},\n", - " 'B': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30', 'type': 'angle'}},\n", - " 'PNV': {'C': {'mrid': '_5bc367a5-f771-494a-83bd-b4560d538df7',\n", + " 'A': {'mrid': '_448ef334-0751-4a27-b168-c9a9bf5d4cb4', 'type': 'angle'},\n", + " 'C': {'mrid': '_d6aabda3-6dcd-4f8e-b9d5-314c112e1bc9', 'type': 'angle'}},\n", + " 'manual close': {'mrid': '_ffe351f9-22c0-483d-8017-04dcbb6c107b',\n", + " 'type': 'magnitude'},\n", + " 'Pos': {'C': {'mrid': '_5220d83e-ee9b-451d-8ce7-2768b6ad8612', 'type': 'pos'},\n", + " 'B': {'mrid': '_bbf78ce5-9c07-4843-b2c0-0d94305fb581', 'type': 'pos'},\n", + " 'A': {'mrid': '_ffe351f9-22c0-483d-8017-04dcbb6c107b', 'type': 'pos'}},\n", + " 'VA': {'B': {'mrid': '_6e76eb6e-363c-4d5d-b304-2e216c9451c0',\n", " 'type': 'angle'},\n", - " 'A': {'mrid': '_806d5de7-541b-4944-8825-c7afffa5abc2', 'type': 'angle'},\n", - " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}}" + " 'C': {'mrid': '_9c51e7a7-3fb7-4bf2-a9f5-6c7aa4330549', 'type': 'angle'},\n", + " 'A': {'mrid': '_f6013f90-37e0-4e01-97b4-ea67cf410711', 'type': 'angle'}}}" ] }, - "execution_count": 185, + "execution_count": 243, "metadata": {}, "output_type": "execute_result" } @@ -772,7 +779,7 @@ }, { "cell_type": "code", - "execution_count": 186, + "execution_count": 244, "metadata": {}, "outputs": [], "source": [ @@ -781,7 +788,7 @@ }, { "cell_type": "code", - "execution_count": 187, + "execution_count": 217, "metadata": {}, "outputs": [ { @@ -791,7 +798,7 @@ "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexclusion_dict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_csv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"exclusion.csv\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDataFrame\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_dict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mexclusion_dict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_csv\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"exclusion.csv\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mNameError\u001b[0m: name 'exclusion_dict' is not defined" ] } @@ -802,12 +809,117 @@ }, { "cell_type": "code", - "execution_count": 188, + "execution_count": 368, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " False True\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + " False True\n", + "False\n", + " False True\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + " False True\n", + "False\n", + " False True\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + " False True\n", + "False\n", + " False True\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n", + "1.0 False False\n", + "False\n" + ] + } + ], + "source": [ + "csv_file = 'DNP3 list.xlsx'\n", + "df = pd.read_excel(csv_file,sheet_name=sheet_name)\n", + "df = df.replace(np.nan, '', regex=True)\n", + "x = []\n", + "\n", + "for index,row in df.iterrows():\n", + " row\n", + " print(row['Multiplier'], pd.isna(row['Multiplier']), row['Multiplier'] == '')\n", + " print(pd.isna(row['Multiplier']))\n", + " if pd.isna(row['Multiplier'] or row['Multiplier'] == ''):\n", + "# print(row[0])\n", + " x.append(index)" + ] + }, + { + "cell_type": "code", + "execution_count": 369, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index 37\n", + "Type Auto/Manual\n", + "Multiplier 1\n", + "CIM attribute \n", + "CIM phase \n", + "CIM units \n", + "Units \n", + "Group \n", + "Variation \n", + "Name: 23, dtype: object" + ] + }, + "execution_count": 369, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "row" + ] + }, + { + "cell_type": "code", + "execution_count": 391, "metadata": {}, "outputs": [], "source": [ "def get_conversion_model(csv_file,sheet_name):\n", " df = pd.read_excel(csv_file,sheet_name=sheet_name)\n", + " df = df.replace(np.nan, '', regex=True)\n", " master_dict ={\n", " 'Analog input':{},\n", " 'Analog output':{},\n", @@ -824,14 +936,16 @@ "\n", " x = []\n", "\n", - " for row in df.iterrows():\n", - " if pd.isna(row[1][2]):\n", - " # print(row[0])\n", - " x.append(row[0])\n", - "# print(df.shape)\n", + " for index, row in df.iterrows():\n", + "# print(pd.isna(row['Multiplier']))\n", + " if pd.isna(row['Multiplier']) or row['Multiplier'] == '':\n", + "# print(index)\n", + " x.append(index)\n", + " print(df.shape)\n", " x.append(df.shape[0])\n", " it = iter(x)\n", " for x in it:\n", + "# print(x)\n", " type_name = df.iloc[x][1]\n", " print(type_name)\n", " next_value = next(it)\n", @@ -839,13 +953,14 @@ " print(pd.DataFrame(df[x+1:next_value]))\n", " temp_df = pd.DataFrame(df[x+1: next_value])\n", " temp_df = temp_df.set_index('Index')\n", + "# temp_df = temp_df.replace(np.nan, '', regex=True)\n", " master_dict[type_name] = temp_df.T.to_dict()\n", " return master_dict\n" ] }, { "cell_type": "code", - "execution_count": 189, + "execution_count": 389, "metadata": {}, "outputs": [], "source": [ @@ -854,36 +969,37 @@ }, { "cell_type": "code", - "execution_count": 190, + "execution_count": 390, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "(21, 8)\n", "Analog input\n", "1 21\n", - " Index type Multiplier MultiplierString Units \\\n", - "1 1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", - "2 2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", - "3 3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", - "4 4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", - "5 5.0 Voltage feedback B (L-L) 0.009155 (300 / 32768) V \n", - "6 6.0 Voltage feedback C (L-L) 0.009155 (300 / 32768) V \n", - "7 7.0 Current feedback (A) 0.000305 (10 / 32768) A \n", - "8 8.0 Current feedback (B) 0.000305 (10 / 32768) A \n", - "9 9.0 Current feedback (C) 0.000305 (10 / 32768) A \n", - "10 10.0 Total 3 ph P 0.137329 (4500 / 32768) W \n", - "11 11.0 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", - "12 12.0 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", - "13 13.0 PF 0.001000 0.001 NaN \n", - "14 13.0 Hz 0.010000 0.01 Hz \n", - "15 36.0 P A 0.137329 (4500/32768) W \n", - "16 37.0 P B 0.137329 (4500/32768) W \n", - "17 38.0 P C 0.137329 (4500/32768) W \n", - "18 39.0 Q A 0.137329 (4500/32768) VARs \n", - "19 40.0 Q B 0.137329 (4500/32768) VARs \n", - "20 41.0 Q C 0.137329 (4500/32768) VARs \n", + " Index Type Multiplier MultiplierString Units \\\n", + "1 1 Voltage feedback A (L-N) 0.00457764 (150 / 32768) V \n", + "2 2 Voltage feedback B (L-N) 0.00457764 (150 / 32768) V \n", + "3 3 Voltage feedback C (L-N) 0.00457764 (150 / 32768) V \n", + "4 4 Voltage feedback A (L-L) 0.00915527 (300 / 32768) V \n", + "5 5 Voltage feedback B (L-L) 0.00915527 (300 / 32768) V \n", + "6 6 Voltage feedback C (L-L) 0.00915527 (300 / 32768) V \n", + "7 7 Current feedback (A) 0.000305176 (10 / 32768) A \n", + "8 8 Current feedback (B) 0.000305176 (10 / 32768) A \n", + "9 9 Current feedback (C) 0.000305176 (10 / 32768) A \n", + "10 10 Total 3 ph P 0.137329 (4500 / 32768) W \n", + "11 11 Total 3 ph Q 0.137329 (4500 / 32768) VARs \n", + "12 12 Total 3 ph VA 0.137329 (4500 / 32768) VA \n", + "13 13 PF 0.001 0.001 \n", + "14 13 Hz 0.01 0.01 Hz \n", + "15 36 P A 0.137329 (4500/32768) W \n", + "16 37 P B 0.137329 (4500/32768) W \n", + "17 38 P C 0.137329 (4500/32768) W \n", + "18 39 Q A 0.137329 (4500/32768) VARs \n", + "19 40 Q B 0.137329 (4500/32768) VARs \n", + "20 41 Q C 0.137329 (4500/32768) VARs \n", "\n", " CIM attribute CIM phase CIM units \n", "1 magnitude A PNV \n", @@ -895,11 +1011,11 @@ "7 x A PNV \n", "8 y B PNV \n", "9 z C PNV \n", - "10 NaN NaN NaN \n", - "11 NaN NaN NaN \n", - "12 NaN NaN NaN \n", - "13 NaN NaN NaN \n", - "14 NaN NaN NaN \n", + "10 \n", + "11 \n", + "12 \n", + "13 \n", + "14 \n", "15 real A VA \n", "16 real B VA \n", "17 real C VA \n", @@ -907,13 +1023,6 @@ "19 imag B VA \n", "20 iamg C VA \n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/jsimpson/.conda/envs/adms36/lib/python3.6/site-packages/ipykernel_launcher.py:34: UserWarning: DataFrame columns are not unique, some columns will be omitted.\n" - ] } ], "source": [ @@ -923,139 +1032,139 @@ }, { "cell_type": "code", - "execution_count": 191, + "execution_count": 378, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1.0: {'type': 'Voltage feedback A (L-N)',\n", + "{1.0: {'Type': 'Voltage feedback A (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V',\n", " 'CIM attribute': 'magnitude',\n", " 'CIM phase': 'A',\n", " 'CIM units': 'PNV'},\n", - " 2.0: {'type': 'Voltage feedback B (L-N)',\n", + " 2.0: {'Type': 'Voltage feedback B (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V',\n", " 'CIM attribute': 'magnitude',\n", " 'CIM phase': 'B',\n", " 'CIM units': 'PNV'},\n", - " 3.0: {'type': 'Voltage feedback C (L-N)',\n", + " 3.0: {'Type': 'Voltage feedback C (L-N)',\n", " 'Multiplier': 0.00457763671,\n", " 'MultiplierString': '(150 / 32768) ',\n", " 'Units': 'V',\n", " 'CIM attribute': 'magnitude',\n", " 'CIM phase': 'C',\n", " 'CIM units': 'PNV'},\n", - " 4.0: {'type': 'Voltage feedback A (L-L)',\n", + " 4.0: {'Type': 'Voltage feedback A (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V',\n", " 'CIM attribute': 'angle',\n", " 'CIM phase': 'A',\n", " 'CIM units': 'PNV'},\n", - " 5.0: {'type': 'Voltage feedback B (L-L)',\n", + " 5.0: {'Type': 'Voltage feedback B (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V',\n", " 'CIM attribute': 'angle',\n", " 'CIM phase': 'B',\n", " 'CIM units': 'PNV'},\n", - " 6.0: {'type': 'Voltage feedback C (L-L)',\n", + " 6.0: {'Type': 'Voltage feedback C (L-L)',\n", " 'Multiplier': 0.00915527343,\n", " 'MultiplierString': '(300 / 32768) ',\n", " 'Units': 'V',\n", " 'CIM attribute': 'angle',\n", " 'CIM phase': 'C',\n", " 'CIM units': 'PNV'},\n", - " 7.0: {'type': 'Current feedback (A)',\n", + " 7.0: {'Type': 'Current feedback (A)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A',\n", " 'CIM attribute': 'x',\n", " 'CIM phase': 'A',\n", " 'CIM units': 'PNV'},\n", - " 8.0: {'type': 'Current feedback (B)',\n", + " 8.0: {'Type': 'Current feedback (B)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A',\n", " 'CIM attribute': 'y',\n", " 'CIM phase': 'B',\n", " 'CIM units': 'PNV'},\n", - " 9.0: {'type': 'Current feedback (C)',\n", + " 9.0: {'Type': 'Current feedback (C)',\n", " 'Multiplier': 0.00030517578,\n", " 'MultiplierString': '(10 / 32768) ',\n", " 'Units': 'A',\n", " 'CIM attribute': 'z',\n", " 'CIM phase': 'C',\n", " 'CIM units': 'PNV'},\n", - " 10.0: {'type': 'Total 3 ph P',\n", + " 10.0: {'Type': 'Total 3 ph P',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'W',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", - " 11.0: {'type': 'Total 3 ph Q',\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", + " 11.0: {'Type': 'Total 3 ph Q',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VARs',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", - " 12.0: {'type': 'Total 3 ph VA',\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", + " 12.0: {'Type': 'Total 3 ph VA',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500 / 32768) ',\n", " 'Units': 'VA',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", - " 13.0: {'type': 'Hz',\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", + " 13.0: {'Type': 'Hz',\n", " 'Multiplier': 0.01,\n", " 'MultiplierString': 0.01,\n", " 'Units': 'Hz',\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan},\n", - " 36.0: {'type': 'P A',\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': ''},\n", + " 36.0: {'Type': 'P A',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", " 'Units': 'W',\n", " 'CIM attribute': 'real',\n", " 'CIM phase': 'A',\n", " 'CIM units': 'VA'},\n", - " 37.0: {'type': 'P B',\n", + " 37.0: {'Type': 'P B',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", " 'Units': 'W',\n", " 'CIM attribute': 'real',\n", " 'CIM phase': 'B',\n", " 'CIM units': 'VA'},\n", - " 38.0: {'type': 'P C',\n", + " 38.0: {'Type': 'P C',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", " 'Units': 'W',\n", " 'CIM attribute': 'real',\n", " 'CIM phase': 'C',\n", " 'CIM units': 'VA'},\n", - " 39.0: {'type': 'Q A',\n", + " 39.0: {'Type': 'Q A',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", " 'Units': 'VARs',\n", " 'CIM attribute': 'imag',\n", " 'CIM phase': 'A',\n", " 'CIM units': 'VA'},\n", - " 40.0: {'type': 'Q B',\n", + " 40.0: {'Type': 'Q B',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", " 'Units': 'VARs',\n", " 'CIM attribute': 'imag',\n", " 'CIM phase': 'B',\n", " 'CIM units': 'VA'},\n", - " 41.0: {'type': 'Q C',\n", + " 41.0: {'Type': 'Q C',\n", " 'Multiplier': 0.13732910156,\n", " 'MultiplierString': '(4500/32768)',\n", " 'Units': 'VARs',\n", @@ -1064,7 +1173,7 @@ " 'CIM units': 'VA'}}" ] }, - "execution_count": 191, + "execution_count": 378, "metadata": {}, "output_type": "execute_result" } @@ -1075,9 +1184,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 379, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (, line 28)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m File \u001b[0;32m\"\"\u001b[0;36m, line \u001b[0;32m28\u001b[0m\n\u001b[0;31m 'object' ='_C1CF0980-10AF-49A5-BD61-F46CC1D92365',\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], "source": [ "# 0\tmanual close\t1\tBinary\t\t\t\t\t\n", "# 1\tmanual open\t1\tBinary\t\t\t\t\t\n", @@ -1115,121 +1233,123 @@ }, { "cell_type": "code", - "execution_count": 192, + "execution_count": 380, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "(24, 9)\n", "Analog input\n", "1 4\n", - " Index Type Multiplier Units CIM attribute \\\n", - "1 0.0 Voltage feedback (secondary) 0.1 V NaN \n", - "2 11111.0 Current feedback (Secondary) 1.0 V NaN \n", - "3 3.0 Primary VArs 1.0 NaN NaN \n", + " Index Type Multiplier Units CIM attribute \\\n", + "1 0 Voltage feedback (secondary) 0.1 V \n", + "2 11111 Current feedback (Secondary) 1 V \n", + "3 3 Primary VArs 1 \n", "\n", - " CIM phase CIM units Group Variation \n", - "1 NaN NaN NaN NaN \n", - "2 NaN NaN NaN NaN \n", - "3 NaN NaN NaN NaN \n", + " CIM phase CIM units Group Variation \n", + "1 \n", + "2 \n", + "3 \n", "Analog output\n", "6 10\n", - " Index Type Multiplier Units CIM attribute CIM phase \\\n", - "6 7.0 Overvoltage Limit 1.0 NaN NaN NaN \n", - "7 8.0 Undervoltage limit 1.0 NaN NaN NaN \n", - "8 11111.0 Close delay time 1.0 NaN NaN NaN \n", - "9 11112.0 open delay time 1.0 NaN NaN NaN \n", + " Index Type Multiplier Units CIM attribute CIM phase \\\n", + "6 7 Overvoltage Limit 1 \n", + "7 8 Undervoltage limit 1 \n", + "8 11111 Close delay time 1 \n", + "9 11112 open delay time 1 \n", "\n", - " CIM units Group Variation \n", - "6 NaN NaN NaN \n", - "7 NaN NaN NaN \n", - "8 NaN NaN NaN \n", - "9 NaN NaN NaN \n", + " CIM units Group Variation \n", + "6 \n", + "7 \n", + "8 \n", + "9 \n", "Binary output\n", "12 15\n", - " Index Type Multiplier Units CIM attribute CIM phase \\\n", - "12 0.0 manual close 1.0 Binary NaN NaN \n", - "13 1.0 manual open 1.0 Binary NaN NaN \n", - "14 11111.0 Scada Control 1.0 Binary NaN NaN \n", + " Index Type Multiplier Units CIM attribute CIM phase CIM units \\\n", + "12 0 manual close 1 Binary \n", + "13 1 manual open 1 Binary \n", + "14 11111 Scada Control 1 Binary \n", "\n", - " CIM units Group Variation \n", - "12 NaN NaN NaN \n", - "13 NaN NaN NaN \n", - "14 NaN NaN NaN \n", + " Group Variation \n", + "12 \n", + "13 \n", + "14 \n", "Binary input\n", "17 24\n", - " Index Type Multiplier Units CIM attribute CIM phase \\\n", - "17 11111.0 Enabled 1.0 Binary NaN NaN \n", - "18 6.0 Remote 1.0 Binary NaN NaN \n", - "19 7.0 Local 1.0 Binary NaN NaN \n", - "20 4.0 Manual 1.0 Binary NaN NaN \n", - "21 5.0 Auto 1.0 Binary NaN NaN \n", - "22 0.0 Close status 1.0 Binary Pos ABC \n", - "23 1.0 Open Status 1.0 Binary Pos ABC \n", + " Index Type Multiplier Units CIM attribute CIM phase CIM units \\\n", + "17 11111 Enabled 1 Binary \n", + "18 6 Remote 1 Binary \n", + "19 7 Local 1 Binary \n", + "20 4 Manual 1 Binary \n", + "21 5 Auto 1 Binary \n", + "22 0 Close status 1 Binary Pos ABC Pos \n", + "23 1 Open Status 1 Binary Pos ABC Pos \n", "\n", - " CIM units Group Variation \n", - "17 NaN NaN NaN \n", - "18 NaN NaN NaN \n", - "19 NaN NaN NaN \n", - "20 NaN NaN NaN \n", - "21 NaN NaN NaN \n", - "22 Pos NaN NaN \n", - "23 Pos NaN NaN \n", + " Group Variation \n", + "17 \n", + "18 \n", + "19 \n", + "20 \n", + "21 \n", + "22 \n", + "23 \n", + "(24, 9)\n", "Analog input\n", "1 4\n", - " Index Type Multiplier CIM attribute \\\n", - "1 0.0 Load Voltage feedback (secondary) 1.0 NaN \n", - "2 2.0 Load Current feedback (Secondary) 1.0 NaN \n", - "3 5.0 Tap position 1.0 Pos \n", + " Index Type Multiplier CIM attribute CIM phase \\\n", + "1 0 Load Voltage feedback (secondary) 1 \n", + "2 2 Load Current feedback (Secondary) 1 \n", + "3 5 Tap position 1 Pos A \n", "\n", - " CIM phase CIM units Units Group Variation \n", - "1 NaN NaN NaN NaN NaN \n", - "2 NaN NaN NaN NaN NaN \n", - "3 A Pos NaN NaN NaN \n", + " CIM units Units Group Variation \n", + "1 \n", + "2 \n", + "3 Pos \n", "Analog output:\n", "6 13\n", - " Index Type Multiplier CIM attribute \\\n", - "6 0.0 Fail safe Voltage set point 1.0 NaN \n", - "7 2.0 Fail Safe Voltage bandwidth (dead band) 1.0 NaN \n", - "8 34.0 Emergency Voltage set point 1.0 NaN \n", - "9 236.0 Emergency Voltage bandwidth  (dead band) 1.0 NaN \n", - "10 15.0 Voltage max 1.0 NaN \n", - "11 14.0 voltage min 1.0 NaN \n", - "12 4.0 Time Delay 1.0 NaN \n", + " Index Type Multiplier CIM attribute \\\n", + "6 0 Fail safe Voltage set point 1 \n", + "7 2 Fail Safe Voltage bandwidth (dead band) 1 \n", + "8 34 Emergency Voltage set point 1 \n", + "9 236 Emergency Voltage bandwidth  (dead band) 1 \n", + "10 15 Voltage max 1 \n", + "11 14 voltage min 1 \n", + "12 4 Time Delay 1 \n", "\n", - " CIM phase CIM units Units Group Variation \n", - "6 NaN NaN NaN NaN NaN \n", - "7 NaN NaN NaN NaN NaN \n", - "8 NaN NaN NaN NaN NaN \n", - "9 NaN NaN NaN NaN NaN \n", - "10 NaN NaN NaN NaN NaN \n", - "11 NaN NaN NaN NaN NaN \n", - "12 NaN NaN NaN NaN NaN \n", + " CIM phase CIM units Units Group Variation \n", + "6 \n", + "7 \n", + "8 \n", + "9 \n", + "10 \n", + "11 \n", + "12 \n", "Binary output:\n", "15 19\n", - " Index Type Multiplier CIM attribute CIM phase CIM units \\\n", - "15 0.0 manual raise 1 Tap 1.0 NaN NaN NaN \n", - "16 1.0 manual Lower 1 Tap 1.0 NaN NaN NaN \n", - "17 5.0 Scada Control 1.0 NaN NaN NaN \n", - "18 277.0 Enabled 1.0 NaN NaN NaN \n", + " Index Type Multiplier CIM attribute CIM phase CIM units \\\n", + "15 0 manual raise 1 Tap 1 Pos A,B,C Pos \n", + "16 1 manual Lower 1 Tap 1 \n", + "17 5 Scada Control 1 \n", + "18 277 Enabled 1 \n", "\n", - " Units Group Variation \n", - "15 NaN NaN NaN \n", - "16 NaN NaN NaN \n", - "17 NaN NaN NaN \n", - "18 NaN NaN NaN \n", + " Units Group Variation \n", + "15 \n", + "16 \n", + "17 \n", + "18 \n", "Binary input:\n", "21 24\n", - " Index Type Multiplier CIM attribute CIM phase CIM units Units \\\n", - "21 11.0 In band 1.0 NaN NaN NaN NaN \n", - "22 27.0 Remote/Local 1.0 NaN NaN NaN NaN \n", - "23 37.0 Auto/Manual 1.0 NaN NaN NaN NaN \n", + " Index Type Multiplier CIM attribute CIM phase CIM units Units \\\n", + "21 11 In band 1 \n", + "22 27 Remote/Local 1 \n", + "23 37 Auto/Manual 1 \n", "\n", - " Group Variation \n", - "21 NaN NaN \n", - "22 NaN NaN \n", - "23 NaN NaN \n" + " Group Variation \n", + "21 \n", + "22 \n", + "23 \n" ] } ], @@ -1246,7 +1366,7 @@ }, { "cell_type": "code", - "execution_count": 193, + "execution_count": 332, "metadata": {}, "outputs": [ { @@ -1270,87 +1390,71 @@ " \n", " \n", " \n", - " type\n", + " Index\n", + " Type\n", " Multiplier\n", - " MultiplierString\n", - " Units\n", " CIM attribute\n", " CIM phase\n", " CIM units\n", - " \n", - " \n", - " Index\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " Units\n", + " Group\n", + " Variation\n", " \n", " \n", " \n", " \n", - " 1.0\n", - " Voltage feedback A (L-N)\n", - " 0.004578\n", - " (150 / 32768)\n", - " V\n", - " magnitude\n", - " A\n", - " PNV\n", - " \n", - " \n", - " 2.0\n", - " Voltage feedback B (L-N)\n", - " 0.004578\n", - " (150 / 32768)\n", - " V\n", - " magnitude\n", - " B\n", - " PNV\n", + " 1\n", + " 0\n", + " Load Voltage feedback (secondary)\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " 3.0\n", - " Voltage feedback C (L-N)\n", - " 0.004578\n", - " (150 / 32768)\n", - " V\n", - " magnitude\n", - " C\n", - " PNV\n", + " 2\n", + " 2\n", + " Load Current feedback (Secondary)\n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " 4.0\n", - " Voltage feedback A (L-L)\n", - " 0.009155\n", - " (300 / 32768)\n", - " V\n", - " angle\n", + " 3\n", + " 5\n", + " Tap position\n", + " 1\n", + " Pos\n", " A\n", - " PNV\n", + " Pos\n", + " \n", + " \n", + " \n", " \n", " \n", "\n", "" ], "text/plain": [ - " type Multiplier MultiplierString Units \\\n", - "Index \n", - "1.0 Voltage feedback A (L-N) 0.004578 (150 / 32768) V \n", - "2.0 Voltage feedback B (L-N) 0.004578 (150 / 32768) V \n", - "3.0 Voltage feedback C (L-N) 0.004578 (150 / 32768) V \n", - "4.0 Voltage feedback A (L-L) 0.009155 (300 / 32768) V \n", + " Index Type Multiplier CIM attribute CIM phase \\\n", + "1 0 Load Voltage feedback (secondary) 1 \n", + "2 2 Load Current feedback (Secondary) 1 \n", + "3 5 Tap position 1 Pos A \n", "\n", - " CIM attribute CIM phase CIM units \n", - "Index \n", - "1.0 magnitude A PNV \n", - "2.0 magnitude B PNV \n", - "3.0 magnitude C PNV \n", - "4.0 angle A PNV " + " CIM units Units Group Variation \n", + "1 \n", + "2 \n", + "3 Pos " ] }, - "execution_count": 193, + "execution_count": 332, "metadata": {}, "output_type": "execute_result" } @@ -1361,7 +1465,7 @@ }, { "cell_type": "code", - "execution_count": 194, + "execution_count": 333, "metadata": {}, "outputs": [ { @@ -1369,31 +1473,31 @@ "text/plain": [ "{0.0: {'Type': 'Load Voltage feedback (secondary)',\n", " 'Multiplier': 1.0,\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan,\n", - " 'Units': nan,\n", - " 'Group': nan,\n", - " 'Variation': nan},\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': '',\n", + " 'Units': '',\n", + " 'Group': '',\n", + " 'Variation': ''},\n", " 2.0: {'Type': 'Load Current feedback\\xa0(Secondary)',\n", " 'Multiplier': 1.0,\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan,\n", - " 'Units': nan,\n", - " 'Group': nan,\n", - " 'Variation': nan},\n", + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': '',\n", + " 'Units': '',\n", + " 'Group': '',\n", + " 'Variation': ''},\n", " 5.0: {'Type': 'Tap position',\n", " 'Multiplier': 1.0,\n", " 'CIM attribute': 'Pos',\n", " 'CIM phase': 'A',\n", " 'CIM units': 'Pos',\n", - " 'Units': nan,\n", - " 'Group': nan,\n", - " 'Variation': nan}}" + " 'Units': '',\n", + " 'Group': '',\n", + " 'Variation': ''}}" ] }, - "execution_count": 194, + "execution_count": 333, "metadata": {}, "output_type": "execute_result" } @@ -1404,7 +1508,7 @@ }, { "cell_type": "code", - "execution_count": 195, + "execution_count": 334, "metadata": {}, "outputs": [ { @@ -1412,15 +1516,15 @@ "text/plain": [ "{'Type': 'Load Voltage feedback (secondary)',\n", " 'Multiplier': 1.0,\n", - " 'CIM attribute': nan,\n", - " 'CIM phase': nan,\n", - " 'CIM units': nan,\n", - " 'Units': nan,\n", - " 'Group': nan,\n", - " 'Variation': nan}" + " 'CIM attribute': '',\n", + " 'CIM phase': '',\n", + " 'CIM units': '',\n", + " 'Units': '',\n", + " 'Group': '',\n", + " 'Variation': ''}" ] }, - "execution_count": 195, + "execution_count": 334, "metadata": {}, "output_type": "execute_result" } @@ -1431,7 +1535,7 @@ }, { "cell_type": "code", - "execution_count": 196, + "execution_count": 335, "metadata": {}, "outputs": [], "source": [ @@ -1442,7 +1546,7 @@ }, { "cell_type": "code", - "execution_count": 197, + "execution_count": 336, "metadata": {}, "outputs": [], "source": [ @@ -1453,17 +1557,22 @@ " CIM_attribute = conversion['Analog input'][index]['CIM attribute']\n", " ## Check if multiplier is na or str\n", " multiplier = conversion['Analog input'][index]['Multiplier']\n", + " if CIM_units not in model:\n", + " print(str(CIM_units) +' not in model')\n", + " return\n", + "\n", " mrid = model[CIM_units][CIM_phase]['mrid']\n", " if type(multiplier) == str:\n", " multiplier = 1\n", "\n", " CIM_value = {'mrid':mrid} \n", - " if CIM_units == 'PNV' or CIM_units == 'VA':\n", - " CIM_value = {'mrid':mrid, 'magnitude':0,'angle':0} \n", + "# if CIM_units == 'PNV' or CIM_units == 'VA':\n", + "# CIM_value = {'mrid':mrid, 'magnitude':0,'angle':0} \n", "\n", " if mrid not in CIM_msg:\n", " CIM_msg[mrid] = CIM_value \n", " CIM_msg[mrid][CIM_attribute] = value * multiplier # times multipier\n", + " \n", "def update_cim_msg_binary(CIM_msg,index,value,conversion,model):\n", "# print(conversion['Binary input'][index])\n", " if 'Binary input' in conversion and index in conversion['Binary input']:\n", @@ -1472,6 +1581,10 @@ " CIM_attribute = conversion['Binary input'][index]['CIM attribute']\n", " ## Check if multiplier is na or str\n", " multiplier = conversion['Binary input'][index]['Multiplier']\n", + " print(type(CIM_units),CIM_units)\n", + " if CIM_units not in model:\n", + " print(str(CIM_units) +' not in model')\n", + " return\n", " for CIM_phase in CIM_phases:\n", " mrid = model[CIM_units][CIM_phase]['mrid']\n", " CIM_value = {'mrid':mrid} \n", @@ -1482,7 +1595,7 @@ }, { "cell_type": "code", - "execution_count": 198, + "execution_count": 337, "metadata": {}, "outputs": [ { @@ -1492,7 +1605,7 @@ " 'Pos': 12.0}}" ] }, - "execution_count": 198, + "execution_count": 337, "metadata": {}, "output_type": "execute_result" } @@ -1515,9 +1628,17 @@ }, { "cell_type": "code", - "execution_count": 199, + "execution_count": 381, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Pos\n", + " Pos\n" + ] + }, { "data": { "text/plain": [ @@ -1529,7 +1650,7 @@ " 'Pos': 1.0}}" ] }, - "execution_count": 199, + "execution_count": 381, "metadata": {}, "output_type": "execute_result" } @@ -1552,7 +1673,7 @@ }, { "cell_type": "code", - "execution_count": 200, + "execution_count": 382, "metadata": {}, "outputs": [ { @@ -1568,23 +1689,17 @@ " 'magnitude': 0.0,\n", " 'angle': 0.0},\n", " '_4044e33a-ecc2-4d3d-8584-f65643c4eea2': {'mrid': '_4044e33a-ecc2-4d3d-8584-f65643c4eea2',\n", - " 'magnitude': 0,\n", - " 'angle': 0,\n", " 'real': 27.465820311999998,\n", " 'imag': 0.0164794921872},\n", " '_ebd287b8-bceb-4471-a709-c82fbd34bd30': {'mrid': '_ebd287b8-bceb-4471-a709-c82fbd34bd30',\n", - " 'magnitude': 0,\n", - " 'angle': 0,\n", " 'real': 0.0,\n", " 'imag': 0.0},\n", " '_11bf885c-b677-42ba-859c-d1a013a9f36e': {'mrid': '_11bf885c-b677-42ba-859c-d1a013a9f36e',\n", - " 'magnitude': 0,\n", - " 'angle': 0,\n", " 'real': 0.0,\n", " 'iamg': 0.0}}" ] }, - "execution_count": 200, + "execution_count": 382, "metadata": {}, "output_type": "execute_result" } @@ -1608,7 +1723,7 @@ }, { "cell_type": "code", - "execution_count": 201, + "execution_count": 340, "metadata": {}, "outputs": [ { @@ -1624,7 +1739,7 @@ " 'B': {'mrid': '_bd5f3945-5d16-4694-896e-92da8a0e4895', 'type': 'angle'}}}" ] }, - "execution_count": 201, + "execution_count": 340, "metadata": {}, "output_type": "execute_result" } @@ -1635,7 +1750,7 @@ }, { "cell_type": "code", - "execution_count": 202, + "execution_count": 290, "metadata": {}, "outputs": [], "source": [ @@ -1644,6 +1759,13 @@ "cim_attribute = conversion['Analog input'][index]['CIM attribute']" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, diff --git a/test/master_main.py b/test/master_main.py index 339bd1b..f4d371b 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -17,9 +17,8 @@ def run_master(device_ip_port_config_all, names): PORT=device_ip_port_dict['port'] DNP3_ADDR= device_ip_port_dict['link_local_addr'] convertion_type=device_ip_port_dict['conversion_type'] - object_name='632633' + object_name=device_ip_port_dict['CIM object'] - elements_to_device = {'632633': 'Shark'} application_1 = MyMaster(HOST=HOST, # "127.0.0.1 LOCAL="0.0.0.0", PORT=int(PORT), From 61b4b18d26dd234698bdd951b17c62d145d21be5 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 13 Jan 2021 16:42:59 -0700 Subject: [PATCH 36/64] Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- dnp3/model_dict.json | 245 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 dnp3/model_dict.json diff --git a/dnp3/model_dict.json b/dnp3/model_dict.json new file mode 100644 index 0000000..dd1e874 --- /dev/null +++ b/dnp3/model_dict.json @@ -0,0 +1,245 @@ +{"feeders":[ +{"name":"ieee13nodeckt", +"mRID":"_49AD8E07-3BF9-A4E2-CB8F-C3722F837B62", +"substation":"IEEE13", +"substationID":"_6C62C905-6FC7-653D-9F1E-1340F974A587", +"subregion":"Small", +"subregionID":"_ABEB635F-729D-24BF-B8A4-E2EF268D8B9E", +"region":"IEEE", +"regionID":"_73C512BD-7249-4F50-50DA-D93849B89C43", +"synchronousmachines":[ +], +"capacitors":[ +{"name":"cap1","mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","CN1":"675","phases":"ABC","kvar_A":200.0,"kvar_B":200.0,"kvar_C":200.0,"nominalVoltage":4160.0,"nomU":4160.0,"phaseConnection":"Y","grounded":true,"enabled":false,"mode":null,"targetValue":0.0,"targetDeadband":0.0,"aVRDelay":0.0,"monitoredName":null,"monitoredClass":null,"monitoredBus":null,"monitoredPhase":null}, +{"name":"cap2","mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","CN1":"611","phases":"C","kvar_A":0.0,"kvar_B":0.0,"kvar_C":100.0,"nominalVoltage":4160.0,"nomU":2400.0,"phaseConnection":"Y","grounded":true,"enabled":false,"mode":null,"targetValue":0.0,"targetDeadband":0.0,"aVRDelay":0.0,"monitoredName":null,"monitoredClass":null,"monitoredBus":null,"monitoredPhase":null} +], +"regulators":[ +{"bankName":"Reg","size":"3","bankPhases":"ABC","tankName":["reg1","reg2","reg3"],"endNumber":[2,2,2],"endPhase":["A","B","C"],"rtcName":["reg1","reg2","reg3"],"mRID":["_6CFE3829-3905-42FD-A9AD-5C70D3D835EF","_C03B9EC2-DD5F-4C39-A4A7-E212CBBC9940","_5B8EA691-0051-4CF2-9A03-140171317E4D"],"monitoredPhase":["A","B","C"],"TapChanger.tculControlMode":["volt","volt","volt"],"highStep":[16,16,16],"lowStep":[-16,-16,-16],"neutralStep":[0,0,0],"normalStep":[0,0,0],"TapChanger.controlEnabled":[true,true,true],"lineDropCompensation":[true,true,true],"ltcFlag":[true,true,true],"RegulatingControl.enabled":[true,true,true],"RegulatingControl.discrete":[true,true,true],"RegulatingControl.mode":["voltage","voltage","voltage"],"step":[10,8,11],"targetValue":[122.0000,122.0000,122.0000],"targetDeadband":[2.0000,2.0000,2.0000],"limitVoltage":[0.0000,0.0000,0.0000],"stepVoltageIncrement":[0.6250,0.6250,0.6250],"neutralU":[2400.0000,2400.0000,2400.0000],"initialDelay":[15.0000,15.0000,15.0000],"subsequentDelay":[2.0000,2.0000,2.0000],"lineDropR":[3.0000,3.0000,3.0000],"lineDropX":[9.0000,9.0000,9.0000],"reverseLineDropR":[0.0000,0.0000,0.0000],"reverseLineDropX":[0.0000,0.0000,0.0000],"ctRating":[700.0000,700.0000,700.0000],"ctRatio":[3500.0000,3500.0000,3500.0000],"ptRatio":[20.0000,20.0000,20.0000]} +], +"solarpanels":[ +{"name":"house","mRID":"_1E58BE0E-7D37-46A5-A8A2-04B56C78E50A","CN1":"house","phases":"BS","ratedS":5000.0,"ratedU":208.0,"p":4000.000,"q":0.000,"maxIFault":1.111}, +{"name":"school","mRID":"_15EEE173-CC90-4888-B9B8-43916D56890C","CN1":"634","phases":"ABC","ratedS":300000.0,"ratedU":480.0,"p":240000.000,"q":0.000,"maxIFault":1.111} +], +"batteries":[ +{"name":"house","mRID":"_D26D2D54-A267-4FC0-B86F-5FC93983784C","CN1":"house","phases":"BS","ratedS":5000.0,"ratedU":208.0,"p":-5000.0,"q":0.0,"ratedE":13500.0,"storedE":5000.0,"batteryState":"Charging","maxIFault":1.111}, +{"name":"school","mRID":"_9E931F3B-0D9D-4CBE-B771-9400202B20E8","CN1":"634","phases":"ABC","ratedS":100000.0,"ratedU":480.0,"p":-100000.0,"q":0.0,"ratedE":200000.0,"storedE":100000.0,"batteryState":"Charging","maxIFault":1.111} +], +"switches":[ +{"name":"671692","mRID":"_517413CB-6977-46FA-8911-C82332E42884","CN1":"671","CN2":"692","phases":"ABC","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false}, +{"name":"sect1","mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","CN1":"684","CN2":"tap","phases":"C","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false} +], +"fuses":[ +{"name":"fuse1","mRID":"_43EF8365-F932-409B-A51E-FBED3F6DFFAA","CN1":"633","CN2":"xf1","phases":"ABC","ratedCurrent":100.0,"breakingCapacity":0.0,"normalOpen":false} +], +"sectionalisers":[ +], +"breakers":[ +{"name":"brkr1","mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","CN1":"650","CN2":"brkr","phases":"ABC","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false} +], +"reclosers":[ +{"name":"rec1","mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","CN1":"632","CN2":"mid","phases":"ABC","ratedCurrent":400.0,"breakingCapacity":400.0,"normalOpen":false} +], +"disconnectors":[ +], +"energyconsumers":[ +{"name":"611","mRID":"_60B55036-DD71-40CE-ADDA-97B8CE7447DC","phases":"C"}, +{"name":"634a","mRID":"_C39149DE-3451-4D33-B4C2-B1E6C6FC9AAB","phases":"A"}, +{"name":"634b","mRID":"_B21C5599-1D00-4FCF-904B-58D9D4CAC49A","phases":"B"}, +{"name":"634c","mRID":"_3B2021A7-4BFC-418D-9C20-BD6838E52CF8","phases":"C"}, +{"name":"645","mRID":"_47E52220-4596-4AF0-9724-0167B72A4DB8","phases":"B"}, +{"name":"646","mRID":"_0BC9183A-9067-4E06-AA5B-48DC2AB30C80","phases":"B"}, +{"name":"652","mRID":"_572BCFC9-E1D6-4419-9A33-EDD284806AF3","phases":"A"}, +{"name":"670a","mRID":"_91A37B4C-C38B-4269-8467-F40870934386","phases":"A"}, +{"name":"670b","mRID":"_70188FF4-04F0-48D2-8706-698D1FD086E6","phases":"B"}, +{"name":"670c","mRID":"_E503BBF8-3C82-4DF7-B6E9-2488ECFCC847","phases":"C"}, +{"name":"671","mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","phases":"ABC"}, +{"name":"675a","mRID":"_9CAAF741-BE1E-4893-8044-1E507B7DDC38","phases":"A"}, +{"name":"675b","mRID":"_8E34333C-2BB8-4631-B072-383F1CA38F60","phases":"B"}, +{"name":"675c","mRID":"_3FFFB3F7-A7AF-49E1-ACEE-449FC73D3CD6","phases":"C"}, +{"name":"692","mRID":"_8CA46F28-84C0-4FE2-B228-EC4080777865","phases":"C"}, +{"name":"house","mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","phases":"s2:s1"} +], +"measurements":[ +{"name":"Breaker_brkr1_State","mRID":"_00ff72f5-628c-462b-bdd1-2dcc1bd519b5","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"Recloser_rec1_Voltage","mRID":"_017f359e-77e5-48ca-9a02-eaa59d14a941","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"Breaker_brkr1_State","mRID":"_04d9f780-ad0c-4205-b94d-531e66087f2d","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_0769c269-2a4f-4e30-a5ae-fa30f7dc271b","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_169CB0D6-0002-457F-9594-7FEB09DA102D","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"692","SimObject":"swt_671692"}, +{"name":"EnergyConsumer_692","mRID":"_0793bcc6-eab5-45d1-891a-973379c5cdec","ConductingEquipment_mRID":"_8CA46F28-84C0-4FE2-B228-EC4080777865","Terminal_mRID":"_9CA4064A-AACD-49DB-9296-7ADC7E195358","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"692","ConnectivityNode":"692","SimObject":"ld_692"}, +{"name":"EnergyConsumer_675a","mRID":"_0c2e8ddb-6043-4721-a276-27fc19e86c04","ConductingEquipment_mRID":"_9CAAF741-BE1E-4893-8044-1E507B7DDC38","Terminal_mRID":"_02A67D2E-58FF-4CAB-9A85-4346E9E08F1B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675a","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_692675_Power","mRID":"_0cffa585-68ec-478f-b38d-28903fb7a5c6","ConductingEquipment_mRID":"_7060D0BB-B30D-4932-8FA1-40820A0FC4D0","Terminal_mRID":"_B4E1784C-5500-47AE-958B-A286CE8C17A5","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"692675","ConnectivityNode":"692","SimObject":"line_692675"}, +{"name":"ACLineSegment_671684_Power","mRID":"_0e3593f1-b1ee-443c-a350-3d89fb29c51e","ConductingEquipment_mRID":"_D34B0D01-B082-4081-A3CC-B68B9B8313A4","Terminal_mRID":"_A4C58654-2972-4A3E-AE65-3868941DA037","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671684","ConnectivityNode":"671","SimObject":"line_671684"}, +{"name":"EnergyConsumer_675b","mRID":"_0f40449d-eacd-4929-8972-1b02646a1ab3","ConductingEquipment_mRID":"_8E34333C-2BB8-4631-B072-383F1CA38F60","Terminal_mRID":"_0A29AEF0-5F49-47AE-A413-9308AA61B72B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675b","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_632645_Voltage","mRID":"_110eb93f-4227-4411-91b7-01c284484423","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_4891537F-4088-48F5-9B1A-8294D50901E4","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"645","SimObject":"line_632645"}, +{"name":"ACLineSegment_632633_Power","mRID":"_11bf885c-b677-42ba-859c-d1a013a9f36e","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"632","SimObject":"line_632633"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_13e6fb7e-d41c-4366-a97c-f33543b4b801","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_645646_Power","mRID":"_1408fa1f-9dc3-4ea9-9240-ddfac7f23864","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_F3C0F2D0-4D28-49B5-A920-37409B99229D","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"645","SimObject":"line_645646"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_1550b167-a168-44ef-ba67-8e029f22b433","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_1671d766-d683-4038-bf23-86ecf752381d","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_BB0411D7-5261-433D-B327-549DA536EEEC","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_611","mRID":"_17dd8a60-a58d-4acf-b19a-4c9b1c7a1c80","ConductingEquipment_mRID":"_60B55036-DD71-40CE-ADDA-97B8CE7447DC","Terminal_mRID":"_2F5C9D9B-8CC5-4636-8BC8-2AC78F01F5BC","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"611","ConnectivityNode":"611","SimObject":"ld_611"}, +{"name":"LoadBreakSwitch_671692_State","mRID":"_18ccedff-3c2c-4c1b-891e-5af708e4421c","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"LoadBreakSwitch_671692_Current","mRID":"_1a0ef275-fbdd-46b3-acdd-de643f6168ab","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"A","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"EnergyConsumer_634b","mRID":"_1bcc8391-d8ce-4c32-9131-163ff1eada50","ConductingEquipment_mRID":"_B21C5599-1D00-4FCF-904B-58D9D4CAC49A","Terminal_mRID":"_272D015C-A83B-457C-8109-5E78E92CB6E4","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634b","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_671","mRID":"_1c575353-e825-4bab-8ca1-04f64602244c","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_1c8b757a-e27a-496b-bfa8-fbd19c2a3292","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"ACLineSegment_632645_Power","mRID":"_23b10aed-23da-4911-8be7-55e1d722d4cf","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_A563A3A5-EA39-4998-96B2-538227E2D2F6","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"632","SimObject":"line_632645"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_243fdef8-003d-4d71-89cf-0f5e1a2e1562","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_BB0411D7-5261-433D-B327-549DA536EEEC","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_2a54cdf8-d469-4ac7-aee9-e49b39d46756","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_650632_Voltage","mRID":"_2e09270d-ba65-4243-ac20-7501f63c9bf0","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"ACLineSegment_650632_Power","mRID":"_2f6db582-9ee9-469b-85ea-ad75e0f9fd7d","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_3064b38f-e8ae-4d40-aa81-fb5c4d52704a","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"Breaker_brkr1_Current","mRID":"_309d665e-b3dd-46b1-9a94-366628cabc42","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"A","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"EnergyConsumer_671","mRID":"_327bce10-4686-43c6-8cde-56306d151b15","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"ACLineSegment_671680_Power","mRID":"_32de2bad-ad0c-4686-9869-94ea00616138","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_8BE8D3EA-EEE9-406C-B24C-B169C8EC799A","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"671","SimObject":"line_671680"}, +{"name":"ACLineSegment_645646_Voltage","mRID":"_32ed617f-263f-4bfe-93ac-a189e0e5d171","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_D02B9E99-3557-48F5-B475-4871E47D08F6","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"646","SimObject":"line_645646"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_3324240e-a64c-4acd-9ff9-db9347e9a250","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_670b","mRID":"_373565da-5ef8-490e-8b42-4a29686c8427","ConductingEquipment_mRID":"_70188FF4-04F0-48D2-8706-698D1FD086E6","Terminal_mRID":"_CCC4E540-46CC-422C-98DA-32898FD31637","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670b","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_37780e7f-0a56-4980-96ac-32670b6145cd","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"EnergyConsumer_house","mRID":"_379cc9ba-b6d7-4739-b8b4-7fbe8768834a","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"PNV","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"EnergyConsumer_671","mRID":"_3892f276-5fcb-44cc-ab7c-1cbb272af2fd","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"Breaker_brkr1_Current","mRID":"_392f7130-efe9-493c-bd42-3a31a556b20d","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"A","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_39615e17-18ee-4304-a0ce-1be657b08602","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerTransformer_xfm1_Voltage","mRID":"_3a304c71-d5ec-4bc1-b118-8f2ace903adc","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"Recloser_rec1_State","mRID":"_3a4d201c-bc7e-4d65-91bc-37e1ea8e7125","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_675c","mRID":"_3ae108c0-5e36-443f-9616-98182c646665","ConductingEquipment_mRID":"_3FFFB3F7-A7AF-49E1-ACEE-449FC73D3CD6","Terminal_mRID":"_72362309-A950-40BC-A38C-E08E1AD53477","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675c","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_3fe5535c-8533-4c43-a741-ade81f143cc6","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"PNV","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"ACLineSegment_632633_Power","mRID":"_4044e33a-ecc2-4d3d-8584-f65643c4eea2","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"632","SimObject":"line_632633"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_448ef334-0751-4a27-b168-c9a9bf5d4cb4","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_692675_Power","mRID":"_45fa2ec2-756b-4d4f-aab8-41d2f9dc8a73","ConductingEquipment_mRID":"_7060D0BB-B30D-4932-8FA1-40820A0FC4D0","Terminal_mRID":"_B4E1784C-5500-47AE-958B-A286CE8C17A5","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"692675","ConnectivityNode":"692","SimObject":"line_692675"}, +{"name":"LinearShuntCompensator_cap2","mRID":"_47c95834-3913-406c-82ba-08374f1b02d2","ConductingEquipment_mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","Terminal_mRID":"_FD3834FF-6C3B-434F-A41D-77EB8237F53D","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap2","ConnectivityNode":"611","SimObject":"cap_cap2"}, +{"name":"PowerTransformer_xfm1_Voltage","mRID":"_48aa1067-ee1d-4763-9845-3ad80be775a0","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"EnergyConsumer_652","mRID":"_4c5466c3-71b6-4491-96f2-e2fac89301ca","ConductingEquipment_mRID":"_572BCFC9-E1D6-4419-9A33-EDD284806AF3","Terminal_mRID":"_02217298-AD5B-463C-A912-957740214701","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"652","ConnectivityNode":"652","SimObject":"ld_652"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_4ea65778-8d96-41b8-9352-00c63d0f337a","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_645","mRID":"_5142fd4b-8371-40b1-85e4-938e372d64a7","ConductingEquipment_mRID":"_47E52220-4596-4AF0-9724-0167B72A4DB8","Terminal_mRID":"_15298D41-D5E5-464F-A96D-01FFCF0DA7DB","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"645","ConnectivityNode":"645","SimObject":"ld_645"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_5220d83e-ee9b-451d-8ce7-2768b6ad8612","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"Recloser_rec1_Voltage","mRID":"_53d39eb5-4602-454f-9fd1-66540399cc39","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_634c","mRID":"_5479191e-e5bc-4d11-ba9e-f210121ac087","ConductingEquipment_mRID":"_3B2021A7-4BFC-418D-9C20-BD6838E52CF8","Terminal_mRID":"_C61A2238-FE09-4406-B172-26CB9F145DB1","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634c","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_675b","mRID":"_55b04aca-c331-4a1d-ad1f-f34ff9188396","ConductingEquipment_mRID":"_8E34333C-2BB8-4631-B072-383F1CA38F60","Terminal_mRID":"_0A29AEF0-5F49-47AE-A413-9308AA61B72B","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675b","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_632670_Voltage","mRID":"_55f6b5ef-d9b6-489b-ac11-4fac69aad17c","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_B554744A-F5B5-485C-B67E-2487A8740295","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"670","SimObject":"line_632670"}, +{"name":"EnergyConsumer_675c","mRID":"_56ec1537-6ded-4851-8c45-ad8bacde74b0","ConductingEquipment_mRID":"_3FFFB3F7-A7AF-49E1-ACEE-449FC73D3CD6","Terminal_mRID":"_72362309-A950-40BC-A38C-E08E1AD53477","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675c","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"ACLineSegment_632633_Voltage","mRID":"_5bc367a5-f771-494a-83bd-b4560d538df7","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_F0E10483-D8AD-46BE-AF5F-08228955796B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"633","SimObject":"line_632633"}, +{"name":"Recloser_rec1_State","mRID":"_5db0c160-f3f0-488e-88a9-5c8ef4ee130a","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"LoadBreakSwitch_671692_Current","mRID":"_5e915fa7-a0c2-4d38-8a7d-2de423b396e7","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"LoadBreakSwitch_sect1_Current","mRID":"_62ee3f81-c7c6-439a-89fe-8fe30daa3bff","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_8F517DFE-D985-4D24-8339-96FA2A789E88","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"684","SimObject":"swt_sect1"}, +{"name":"EnergyConsumer_634c","mRID":"_632cbbe7-057c-4106-9197-2c70f709e5a5","ConductingEquipment_mRID":"_3B2021A7-4BFC-418D-9C20-BD6838E52CF8","Terminal_mRID":"_C61A2238-FE09-4406-B172-26CB9F145DB1","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634c","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_634a","mRID":"_634b1b30-e4b4-4ab6-9b19-3d5153df2313","ConductingEquipment_mRID":"_C39149DE-3451-4D33-B4C2-B1E6C6FC9AAB","Terminal_mRID":"_D0BA8E68-F269-4382-B1D0-8EA25663C9AA","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634a","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"EnergyConsumer_670c","mRID":"_63dc721e-038b-4068-ab4c-8cf2b5e1aef5","ConductingEquipment_mRID":"_E503BBF8-3C82-4DF7-B6E9-2488ECFCC847","Terminal_mRID":"_F421B562-1AF9-42C1-8906-D80CABA91FC4","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670c","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"EnergyConsumer_670a","mRID":"_65398be0-9abe-44d6-9ef7-359238a371e8","ConductingEquipment_mRID":"_91A37B4C-C38B-4269-8467-F40870934386","Terminal_mRID":"_83481773-E9FC-4720-89B4-796C5081D518","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670a","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"ACLineSegment_684611_Power","mRID":"_65d1b173-43d6-4927-b495-bb61f11518c5","ConductingEquipment_mRID":"_8E180773-2A9B-4136-BC9A-132A52C8E276","Terminal_mRID":"_EA5FF2C4-F7E1-436D-BDEC-1C48D4DBB50D","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"684611","ConnectivityNode":"tap","SimObject":"line_684611"}, +{"name":"ACLineSegment_632670_Power","mRID":"_66bb7d46-33ff-41e1-b58c-9b404e0ab21b","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_5EDB2918-D669-457C-9F44-E08EBA5AC850","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"mid","SimObject":"line_632670"}, +{"name":"Recloser_rec1_Voltage","mRID":"_6e2444b6-606a-4ab6-9db1-29dec0be69bc","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_DD4717B2-5FCD-4E67-9BC8-DD307B80AFEA","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"mid","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_634a","mRID":"_6e304d52-985a-4cb7-8b38-7783f935d975","ConductingEquipment_mRID":"_C39149DE-3451-4D33-B4C2-B1E6C6FC9AAB","Terminal_mRID":"_D0BA8E68-F269-4382-B1D0-8EA25663C9AA","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634a","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_6e76eb6e-363c-4d5d-b304-2e216c9451c0","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_6e929a30-05fc-4237-beb4-8c22613d6b9b","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_704cfb22-8d38-4f2e-8fc6-4f27fa5af2a0","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"EnergyConsumer_645","mRID":"_711494f2-af31-4b64-bc60-876f364fbb74","ConductingEquipment_mRID":"_47E52220-4596-4AF0-9724-0167B72A4DB8","Terminal_mRID":"_15298D41-D5E5-464F-A96D-01FFCF0DA7DB","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"645","ConnectivityNode":"645","SimObject":"ld_645"}, +{"name":"Recloser_rec1_Current","mRID":"_72ccaae8-52e9-4237-86ec-1e5bc4dc328f","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"A","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_7746f8fa-be50-4100-970e-4f19d8597eb8","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_169CB0D6-0002-457F-9594-7FEB09DA102D","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"692","SimObject":"swt_671692"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_7772ef3a-ca5c-44a8-9fc9-c88d318e851c","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"EnergyConsumer_670b","mRID":"_7e26bf88-6547-423e-ad35-4ee2bcf69402","ConductingEquipment_mRID":"_70188FF4-04F0-48D2-8706-698D1FD086E6","Terminal_mRID":"_CCC4E540-46CC-422C-98DA-32898FD31637","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670b","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"ACLineSegment_632670_Power","mRID":"_7e7b8d30-4b84-4305-bfd7-de577f9d98c7","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_5EDB2918-D669-457C-9F44-E08EBA5AC850","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"mid","SimObject":"line_632670"}, +{"name":"LoadBreakSwitch_671692_State","mRID":"_7ed3d06a-a849-4032-a1ef-10d89688ff33","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"ACLineSegment_632633_Voltage","mRID":"_806d5de7-541b-4944-8825-c7afffa5abc2","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_F0E10483-D8AD-46BE-AF5F-08228955796B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"633","SimObject":"line_632633"}, +{"name":"RatioTapChanger_Reg","mRID":"_824c02fc-18d1-4064-b313-22f9f6ce472b","ConductingEquipment_mRID":"_67B57539-590B-4158-9CBB-9DBA2FE6C1F0","Terminal_mRID":"_5A1120EE-0F2A-4698-9FF5-0F0F4F507BAF","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"Reg","ConnectivityNode":"rg60","SimObject":"reg_Reg"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_828ec85a-475f-4836-8371-66ad57d52a2c","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"VA","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_8502efde-b8ce-47b4-afde-b9661485bc0f","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_85c76d73-8662-41f7-a22b-eb1a9e44c0e6","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"ACLineSegment_632670_Voltage","mRID":"_87a43339-1324-40c3-807c-f9f601839c71","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_B554744A-F5B5-485C-B67E-2487A8740295","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"670","SimObject":"line_632670"}, +{"name":"ACLineSegment_684652_Power","mRID":"_8a558465-50bf-4b1b-95b7-2a11c228fe25","ConductingEquipment_mRID":"_ABF877D7-DAC2-4BF0-AB58-9A8A02E92EB3","Terminal_mRID":"_3313C5A7-ED81-47D3-8114-AD654A5DE21B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"684652","ConnectivityNode":"684","SimObject":"line_684652"}, +{"name":"EnergyConsumer_671","mRID":"_8aef86ba-9013-4992-ba27-763b68cc2b2a","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"ACLineSegment_645646_Voltage","mRID":"_8b549f6b-9c61-419f-8315-5af53d6d1fcb","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_D02B9E99-3557-48F5-B475-4871E47D08F6","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"646","SimObject":"line_645646"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_8e729069-0abb-426c-99b0-1d71491d51dd","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"LoadBreakSwitch_sect1_Voltage","mRID":"_8f6a24ef-83d6-4207-8419-0e076350c51d","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_02AE202B-D91B-4EAD-8EC9-3B87BDD67C8B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"tap","SimObject":"swt_sect1"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_9164bc46-2b05-4270-ae85-3d4bb96d6701","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_2847E06B-C8ED-41E6-B515-C61C9E8EB4B4","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"brkr","SimObject":"swt_brkr1"}, +{"name":"LinearShuntCompensator_cap2","mRID":"_91c1c7d7-e0f5-44ce-9986-2042a4e6a29e","ConductingEquipment_mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","Terminal_mRID":"_FD3834FF-6C3B-434F-A41D-77EB8237F53D","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap2","ConnectivityNode":"611","SimObject":"cap_cap2"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_91fbcb9d-a87c-4d5c-b2b0-e9342c91fcb0","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_E1763FE3-82C2-4434-9424-72DF850D982E","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"634","SimObject":"xf_xfm1"}, +{"name":"EnergyConsumer_670a","mRID":"_93e3dd38-7963-427a-a69a-dac60506ceee","ConductingEquipment_mRID":"_91A37B4C-C38B-4269-8467-F40870934386","Terminal_mRID":"_83481773-E9FC-4720-89B4-796C5081D518","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670a","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"EnergyConsumer_646","mRID":"_9555f572-c16e-4492-9c7a-b297195f1980","ConductingEquipment_mRID":"_0BC9183A-9067-4E06-AA5B-48DC2AB30C80","Terminal_mRID":"_579A122E-A262-4830-88C6-1ADDB81761B1","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"646","ConnectivityNode":"646","SimObject":"ld_646"}, +{"name":"ACLineSegment_671680_Power","mRID":"_99d5cba2-4faf-40a9-ac4c-2e27d29b41fa","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_8BE8D3EA-EEE9-406C-B24C-B169C8EC799A","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"671","SimObject":"line_671680"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_9b6d0140-9fc7-4fb0-ada1-ae0ba8d0d822","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"PNV","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_9c51e7a7-3fb7-4bf2-a9f5-6c7aa4330549","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_9d38c431-a309-4835-824e-ce86464ca9ca","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_670671_Power","mRID":"_9e3dfe03-12b4-4643-a17e-0c2158004a51","ConductingEquipment_mRID":"_45395C84-F20A-4F5A-977F-B80348256421","Terminal_mRID":"_EEF69C71-5A87-4378-8DF4-4578635C81B4","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"670671","ConnectivityNode":"670","SimObject":"line_670671"}, +{"name":"Recloser_rec1_Voltage","mRID":"_9e521176-ce36-4e35-9cd8-9c571a809e87","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_DD4717B2-5FCD-4E67-9BC8-DD307B80AFEA","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"mid","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_652","mRID":"_a1d5af7d-1cda-4aa4-bb97-b1add95b94b2","ConductingEquipment_mRID":"_572BCFC9-E1D6-4419-9A33-EDD284806AF3","Terminal_mRID":"_02217298-AD5B-463C-A912-957740214701","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"652","ConnectivityNode":"652","SimObject":"ld_652"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_a36aab82-53b4-41d0-9161-42b31f8e67a2","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_BB0411D7-5261-433D-B327-549DA536EEEC","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650","SimObject":"xf_sub3"}, +{"name":"ACLineSegment_645646_Power","mRID":"_a497b2aa-65f8-42bb-bfcf-cdcf7897c2da","ConductingEquipment_mRID":"_0BBD0EA3-F665-465B-86FD-FC8B8466AD53","Terminal_mRID":"_F3C0F2D0-4D28-49B5-A920-37409B99229D","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"645646","ConnectivityNode":"645","SimObject":"line_645646"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_school","mRID":"_a5dca590-c8e8-4c09-afc0-ddf636d945bb","ConductingEquipment_mRID":"_D2E930A7-B136-4ACA-A996-8DB5C60AADF3","Terminal_mRID":"_A55AC772-D3F2-48AF-8C84-E80F43198C72","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"ACLineSegment_650632_Power","mRID":"_a6a91863-2e4a-4bd1-98f2-a31a12daebfb","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"ACLineSegment_632645_Voltage","mRID":"_a6cf3a73-cf4b-440a-a7f9-90bd41a55778","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_4891537F-4088-48F5-9B1A-8294D50901E4","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"645","SimObject":"line_632645"}, +{"name":"EnergyConsumer_house","mRID":"_a84b5965-6160-4482-aebf-faf0f2534f89","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"VA","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_a8cf16f8-1240-4438-a416-ea7d0e1f7065","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"ACLineSegment_670671_Power","mRID":"_a8e42300-16e7-4024-be50-4a7dc46857cd","ConductingEquipment_mRID":"_45395C84-F20A-4F5A-977F-B80348256421","Terminal_mRID":"_EEF69C71-5A87-4378-8DF4-4578635C81B4","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"670671","ConnectivityNode":"670","SimObject":"line_670671"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_ab4f355f-2872-4a6d-824b-180ee0002601","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_abb9b6c5-0a60-413f-a8b9-a7ae6c7166a5","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_169CB0D6-0002-457F-9594-7FEB09DA102D","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"692","SimObject":"swt_671692"}, +{"name":"LoadBreakSwitch_671692_State","mRID":"_aed74869-30b6-4a4b-9d1b-5488173e9859","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"PowerTransformer_xfm1_Voltage","mRID":"_aee33dac-79ae-4b41-871f-6009b9a0f389","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_AF475341-BBE8-4C30-A30E-B6EBE9768322","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"xf1","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_671680_Voltage","mRID":"_b291ff81-6813-4d4f-849f-b3d2bc1ba2e9","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_FF3DE145-0066-4C1F-867C-2A2BF3012EDF","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"680","SimObject":"line_671680"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_b4423af0-dfe0-4524-b5eb-bf18d447909a","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"PNV","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_b49a5b70-fe22-405d-8519-de87c8132c9e","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_E1763FE3-82C2-4434-9424-72DF850D982E","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"634","SimObject":"xf_xfm1"}, +{"name":"LinearShuntCompensator_cap2","mRID":"_b5358da9-c206-4149-84ea-8b996495d574","ConductingEquipment_mRID":"_9D725810-BFD6-44C6-961A-2BC027F6FC95","Terminal_mRID":"_FD3834FF-6C3B-434F-A41D-77EB8237F53D","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap2","ConnectivityNode":"611","SimObject":"cap_cap2"}, +{"name":"LoadBreakSwitch_671692_Current","mRID":"_b60dc6a5-90ae-4c96-bdf0-d4968156e13d","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"A","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"ACLineSegment_671680_Power","mRID":"_ba58f48b-42f3-40b1-a6ce-fa4ae2baddb5","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_8BE8D3EA-EEE9-406C-B24C-B169C8EC799A","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"671","SimObject":"line_671680"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_bbf78ce5-9c07-4843-b2c0-0d94305fb581","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_632633_Voltage","mRID":"_bd5f3945-5d16-4694-896e-92da8a0e4895","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_F0E10483-D8AD-46BE-AF5F-08228955796B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"633","SimObject":"line_632633"}, +{"name":"EnergyConsumer_670c","mRID":"_bea2439d-6beb-4222-9c66-5f5ef80504c5","ConductingEquipment_mRID":"_E503BBF8-3C82-4DF7-B6E9-2488ECFCC847","Terminal_mRID":"_F421B562-1AF9-42C1-8906-D80CABA91FC4","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"670c","ConnectivityNode":"670","SimObject":"ld_670c"}, +{"name":"ACLineSegment_671680_Voltage","mRID":"_bf97187c-553c-4816-a2e2-991c6fe63383","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_FF3DE145-0066-4C1F-867C-2A2BF3012EDF","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"680","SimObject":"line_671680"}, +{"name":"Recloser_rec1_Current","mRID":"_bfc5ab56-2fb5-4016-a73e-412708e56077","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"A","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"ACLineSegment_684652_Voltage","mRID":"_c08480ec-4d45-4315-bcd6-eee8a194220d","ConductingEquipment_mRID":"_ABF877D7-DAC2-4BF0-AB58-9A8A02E92EB3","Terminal_mRID":"_8692A804-428A-4637-BD47-69C76F873DAB","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"684652","ConnectivityNode":"652","SimObject":"line_684652"}, +{"name":"EnergyConsumer_house","mRID":"_c1da6db6-8258-460d-868a-751b21cdfdb2","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"PNV","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"LoadBreakSwitch_sect1_State","mRID":"_c24f82e9-d6fa-4837-8ccd-cb5990ab2ad2","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_8F517DFE-D985-4D24-8339-96FA2A789E88","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"684","SimObject":"swt_sect1"}, +{"name":"EnergyConsumer_675a","mRID":"_c38705d2-c899-4e4f-b1b2-41c72b248f52","ConductingEquipment_mRID":"_9CAAF741-BE1E-4893-8044-1E507B7DDC38","Terminal_mRID":"_02A67D2E-58FF-4CAB-9A85-4346E9E08F1B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"675a","ConnectivityNode":"675","SimObject":"ld_675b"}, +{"name":"RatioTapChanger_Reg","mRID":"_c389843f-37c3-4c76-8e54-f02b70368492","ConductingEquipment_mRID":"_67B57539-590B-4158-9CBB-9DBA2FE6C1F0","Terminal_mRID":"_02CD5661-C654-43F6-8D1A-0D666070BB81","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"Reg","ConnectivityNode":"rg60","SimObject":"reg_Reg"}, +{"name":"ACLineSegment_650632_Voltage","mRID":"_c4957103-316a-4e6b-b121-b37034c54dfd","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_c4d0cbd6-0087-43b7-b8d3-41215f4486d8","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"ACLineSegment_671684_Power","mRID":"_c4f39806-439c-4143-80bc-bb5c4a28d4fc","ConductingEquipment_mRID":"_D34B0D01-B082-4081-A3CC-B68B9B8313A4","Terminal_mRID":"_A4C58654-2972-4A3E-AE65-3868941DA037","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671684","ConnectivityNode":"671","SimObject":"line_671684"}, +{"name":"ACLineSegment_650632_Voltage","mRID":"_c6335ec4-f447-4e04-ac5a-2053fd9cc7c9","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_ca8de879-89fb-44c4-a8c5-97889e0dbffd","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_671","mRID":"_cb3c7168-1741-4444-94ae-2e4e147cd703","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"Recloser_rec1_Current","mRID":"_cbb0db9a-3cb0-4a2b-a6c1-f20ea121409b","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_ccfb98a6-1db7-4000-9b2b-d44db4fc3859","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_cd5b34ea-8e3e-4807-bf04-3500679f331a","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"VA","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"Breaker_brkr1_State","mRID":"_cdc67714-d8f3-4a95-a3b7-ce4c95d6b9ed","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_ce725f7b-341f-44ee-9f83-dfca4a0dc4ca","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_2847E06B-C8ED-41E6-B515-C61C9E8EB4B4","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"brkr","SimObject":"swt_brkr1"}, +{"name":"Breaker_brkr1_Current","mRID":"_ceb0ca47-3fed-4e17-9668-9ab813d73ede","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_1D81C7FE-E88F-41E3-A900-476CA6476CCD","measurementType":"A","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"650","SimObject":"swt_brkr1"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_d0cf9cd9-bc5b-4665-a39c-3b47b3a7fa88","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_sub3_Power","mRID":"_d10a93da-687a-496c-8406-3b5166ee3479","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_F6CE3095-9089-4B61-9C13-C7647ADBA888","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"650z","SimObject":"xf_sub3"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_d43b62ec-137c-4887-92f2-5536062954bb","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_d6aabda3-6dcd-4f8e-b9d5-314c112e1bc9","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"ACLineSegment_671684_Voltage","mRID":"_d6bd533e-889d-4c2d-a349-89f56f886363","ConductingEquipment_mRID":"_D34B0D01-B082-4081-A3CC-B68B9B8313A4","Terminal_mRID":"_BC623628-BD23-4A1A-A406-15CA63556149","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671684","ConnectivityNode":"684","SimObject":"line_671684"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_d6bd74bb-8d1f-464e-80aa-b5e96e27a0fa","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"Breaker_brkr1_Voltage","mRID":"_d9c3d91b-6ac2-4927-aaa7-16b85b2c5241","ConductingEquipment_mRID":"_52DE9189-20DC-4C73-BDEE-E960FE1F9493","Terminal_mRID":"_2847E06B-C8ED-41E6-B515-C61C9E8EB4B4","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Breaker","ConductingEquipment_name":"brkr1","ConnectivityNode":"brkr","SimObject":"swt_brkr1"}, +{"name":"LoadBreakSwitch_sect1_Voltage","mRID":"_da33e6e4-b008-4a45-938b-008c9255c49e","ConductingEquipment_mRID":"_2858B6C2-0886-4269-884C-06FA8B887319","Terminal_mRID":"_8F517DFE-D985-4D24-8339-96FA2A789E88","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"sect1","ConnectivityNode":"684","SimObject":"swt_sect1"}, +{"name":"ACLineSegment_632670_Power","mRID":"_dabc5297-9462-4faf-80a4-768042918a41","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_5EDB2918-D669-457C-9F44-E08EBA5AC850","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"mid","SimObject":"line_632670"}, +{"name":"ACLineSegment_632645_Power","mRID":"_db63dc39-157b-4f71-b239-38880b5fcc8d","ConductingEquipment_mRID":"_1C6781A2-5B9D-4525-8A9B-F9B32C4C4AC0","Terminal_mRID":"_A563A3A5-EA39-4998-96B2-538227E2D2F6","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632645","ConnectivityNode":"632","SimObject":"line_632645"}, +{"name":"EnergyConsumer_646","mRID":"_db9565da-e5a1-4829-8f65-7d7bc2619e82","ConductingEquipment_mRID":"_0BC9183A-9067-4E06-AA5B-48DC2AB30C80","Terminal_mRID":"_579A122E-A262-4830-88C6-1ADDB81761B1","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"646","ConnectivityNode":"646","SimObject":"ld_646"}, +{"name":"EnergyConsumer_634b","mRID":"_e03043c2-82dd-4ea2-b635-3642b245a231","ConductingEquipment_mRID":"_B21C5599-1D00-4FCF-904B-58D9D4CAC49A","Terminal_mRID":"_272D015C-A83B-457C-8109-5E78E92CB6E4","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"634b","ConnectivityNode":"634","SimObject":"ld_634c"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_e1881b77-3cad-408b-8b1f-2149f3e02577","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"PNV","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"EnergyConsumer_house","mRID":"_e1bb8270-e341-48da-8414-eaaa7d3fd831","ConductingEquipment_mRID":"_32F02D2B-EE6E-4D3F-8486-1B5CAEF70204","Terminal_mRID":"_2128BB42-3E2D-490A-A29D-05549E81F25D","measurementType":"VA","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"ld_house"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_e250f84f-0131-45cb-a9fe-a9f07ec05250","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_e4789bc7-afc2-49a4-8f4f-70b269f0d70d","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_e7659ef6-cd75-4924-822c-8280793cc206","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_e8a3c4f5-e7c7-402e-bfc1-59ddae5bf6a9","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"Recloser_rec1_State","mRID":"_ea48bd8d-4916-4fee-b424-fc967d6a7d90","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"Pos","phases":"B","MeasurementClass":"Discrete","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"EnergyConsumer_692","mRID":"_ea7f7fe2-21cd-4d00-8d50-0f7a1a53c4e7","ConductingEquipment_mRID":"_8CA46F28-84C0-4FE2-B228-EC4080777865","Terminal_mRID":"_9CA4064A-AACD-49DB-9296-7ADC7E195358","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"692","ConnectivityNode":"692","SimObject":"ld_692"}, +{"name":"PowerTransformer_xfm1_Power","mRID":"_ea8aa437-fb7f-4c76-aa69-6419333a28b8","ConductingEquipment_mRID":"_1E6B5C97-C4E8-4CED-B9A5-6E69F389DA93","Terminal_mRID":"_E1763FE3-82C2-4434-9424-72DF850D982E","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"xfm1","ConnectivityNode":"634","SimObject":"xf_xfm1"}, +{"name":"ACLineSegment_632633_Power","mRID":"_ebd287b8-bceb-4471-a709-c82fbd34bd30","ConductingEquipment_mRID":"_FBE26B35-13AB-457D-9795-DF58B28E309D","Terminal_mRID":"_A6C57C2E-A084-4701-ADF5-FB6F3E36F45C","measurementType":"VA","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632633","ConnectivityNode":"632","SimObject":"line_632633"}, +{"name":"ACLineSegment_670671_Power","mRID":"_ed258ce0-a14c-4cdf-8795-800acc5f5ad4","ConductingEquipment_mRID":"_45395C84-F20A-4F5A-977F-B80348256421","Terminal_mRID":"_EEF69C71-5A87-4378-8DF4-4578635C81B4","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"670671","ConnectivityNode":"670","SimObject":"line_670671"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_ed98c9d7-62c1-4c14-b7b8-724e248cdd1b","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"EnergyConsumer_611","mRID":"_edf4deaa-1850-46c8-8f4e-1b3a4dca9615","ConductingEquipment_mRID":"_60B55036-DD71-40CE-ADDA-97B8CE7447DC","Terminal_mRID":"_2F5C9D9B-8CC5-4636-8BC8-2AC78F01F5BC","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"611","ConnectivityNode":"611","SimObject":"ld_611"}, +{"name":"PowerElectronicsConnection_BatteryUnit_house","mRID":"_ef2ca369-2fb7-40ba-8d93-e77018e54c37","ConductingEquipment_mRID":"_682AB7A9-4FBF-4204-BDE1-27EAB3425DA0","Terminal_mRID":"_A4B4C25C-8744-4B8B-A612-19B80AFA110D","measurementType":"VA","phases":"s1","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"PowerTransformer_sub3_Voltage","mRID":"_f08eb586-8207-4662-a6bb-7ff60f505a55","ConductingEquipment_mRID":"_259E820F-B4AF-4E1A-8271-687534EDAECC","Terminal_mRID":"_717DFB1A-A300-444D-8D3B-0A093EAAD47B","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"sub3","ConnectivityNode":"sourcebus","SimObject":"xf_sub3"}, +{"name":"EnergyConsumer_671","mRID":"_f2064797-04d7-4f5d-bf49-e8ce80261cce","ConductingEquipment_mRID":"_E26D83A0-D29D-41EF-9528-02C882FFCC0D","Terminal_mRID":"_F3A001BA-C6BE-450E-B572-B6D5819808EA","measurementType":"PNV","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"EnergyConsumer","ConductingEquipment_name":"671","ConnectivityNode":"671","SimObject":"ld_671"}, +{"name":"LoadBreakSwitch_671692_Voltage","mRID":"_f4459247-b69b-416a-853e-6021b8747597","ConductingEquipment_mRID":"_517413CB-6977-46FA-8911-C82332E42884","Terminal_mRID":"_F1D6C919-22FA-4E94-81B1-36823F5A9FF5","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"LoadBreakSwitch","ConductingEquipment_name":"671692","ConnectivityNode":"671","SimObject":"swt_671692"}, +{"name":"PowerElectronicsConnection_BatteryUnit_school","mRID":"_f500b9cc-932c-44f1-96b6-12ca0a9ba911","ConductingEquipment_mRID":"_7B671984-4C56-4FF1-9733-B4B6FCA5F2AA","Terminal_mRID":"_90D2243E-05D4-432C-A23B-A6B8D732B027","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"school","ConnectivityNode":"634","SimObject":"634_stmtr"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_f6013f90-37e0-4e01-97b4-ea67cf410711","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"}, +{"name":"Recloser_rec1_Voltage","mRID":"_f6ab2b5f-d5be-4a6c-b8b5-5baa808956de","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_EBAEF4A4-5D80-4FAD-A133-AA18607F3C31","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"632","SimObject":"swt_rec1"}, +{"name":"ACLineSegment_650632_Power","mRID":"_f7024843-072d-43ab-ac84-48f6deb5b70f","ConductingEquipment_mRID":"_A04CDFB1-E951-4FC4-8882-0323CD70AE3C","Terminal_mRID":"_000BA1F2-6879-47D7-826D-3CACD23EE20B","measurementType":"VA","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"650632","ConnectivityNode":"rg60","SimObject":"line_650632"}, +{"name":"ACLineSegment_692675_Power","mRID":"_f73abf1a-512c-4e81-8623-e8498e368e02","ConductingEquipment_mRID":"_7060D0BB-B30D-4932-8FA1-40820A0FC4D0","Terminal_mRID":"_B4E1784C-5500-47AE-958B-A286CE8C17A5","measurementType":"VA","phases":"C","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"692675","ConnectivityNode":"692","SimObject":"line_692675"}, +{"name":"PowerElectronicsConnection_PhotovoltaicUnit_house","mRID":"_fa87b12f-dbd6-4004-bcf4-17df5bb388cc","ConductingEquipment_mRID":"_CEC0FC3A-0FD1-4F1C-9C51-7D9BEF4D8222","Terminal_mRID":"_337F7A56-C530-4F05-8586-940E8460993A","measurementType":"VA","phases":"s2","MeasurementClass":"Analog","ConductingEquipment_type":"PowerElectronicsConnection","ConductingEquipment_name":"house","ConnectivityNode":"house","SimObject":"house_stmtr"}, +{"name":"ACLineSegment_671680_Voltage","mRID":"_fc17a174-ca71-4af1-bb86-c9fda7ec7eca","ConductingEquipment_mRID":"_4C04F838-62AA-475E-AEFA-A63B7C889C13","Terminal_mRID":"_FF3DE145-0066-4C1F-867C-2A2BF3012EDF","measurementType":"PNV","phases":"A","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"671680","ConnectivityNode":"680","SimObject":"line_671680"}, +{"name":"ACLineSegment_632670_Voltage","mRID":"_fc8c789d-752a-413e-94e6-6bc34728fa5b","ConductingEquipment_mRID":"_ABF53597-A808-422A-B7EE-552F24D83A5F","Terminal_mRID":"_B554744A-F5B5-485C-B67E-2487A8740295","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"ACLineSegment","ConductingEquipment_name":"632670","ConnectivityNode":"670","SimObject":"line_632670"}, +{"name":"Recloser_rec1_Voltage","mRID":"_fcfe2f82-3062-4f2a-bcfb-0fb7d2b45e8a","ConductingEquipment_mRID":"_CE5D0651-676B-4AF3-8D67-41BF1B33E30C","Terminal_mRID":"_DD4717B2-5FCD-4E67-9BC8-DD307B80AFEA","measurementType":"PNV","phases":"B","MeasurementClass":"Analog","ConductingEquipment_type":"Recloser","ConductingEquipment_name":"rec1","ConnectivityNode":"mid","SimObject":"swt_rec1"}, +{"name":"RatioTapChanger_Reg","mRID":"_fea855f6-4939-4c6d-8d84-02757a82c01d","ConductingEquipment_mRID":"_67B57539-590B-4158-9CBB-9DBA2FE6C1F0","Terminal_mRID":"_F5258B9A-B51E-40DD-9A06-5918E41F3C35","measurementType":"Pos","phases":"C","MeasurementClass":"Discrete","ConductingEquipment_type":"PowerTransformer","ConductingEquipment_name":"Reg","ConnectivityNode":"rg60","SimObject":"reg_Reg"}, +{"name":"LinearShuntCompensator_cap1","mRID":"_ffe351f9-22c0-483d-8017-04dcbb6c107b","ConductingEquipment_mRID":"_A9DE8829-58CB-4750-B2A2-672846A89753","Terminal_mRID":"_5B38070B-E918-4EAA-8BDB-8B3CE4F8A917","measurementType":"Pos","phases":"A","MeasurementClass":"Discrete","ConductingEquipment_type":"LinearShuntCompensator","ConductingEquipment_name":"cap1","ConnectivityNode":"675","SimObject":"cap_cap1"} +] +}]} From c85f1a7a332f6239905cbedafe7edcc3cff92172 Mon Sep 17 00:00:00 2001 From: SCADA Control Rm Date: Wed, 10 Feb 2021 10:01:11 -0700 Subject: [PATCH 37/64] Updated to add local address --- dnp3/service/dnp3/master.py | 12 +++++++++++- test/master_cmd.py | 8 ++++---- test/master_main.py | 12 +++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index f81a328..85691b6 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -86,6 +86,7 @@ def __init__(self, LOCAL= "0.0.0.0", PORT=20000, DNP3_ADDR=1, + LocalAddr=0, log_handler=asiodnp3.ConsoleLogger().Create(), listener=asiodnp3.PrintingChannelListener().Create(), soe_handler=asiodnp3.PrintingSOEHandler().Create(), @@ -114,6 +115,7 @@ def __init__(self, self.stack_config.master.responseTimeout = openpal.TimeDuration().Seconds(2) self.stack_config.link.RemoteAddr = 1024 ## TODO get from config Was 10 self.stack_config.link.RemoteAddr = DNP3_ADDR + self.stack_config.link.LocalAddr = LocalAddr print('') _log.debug('Adding the master to the channel.') @@ -285,6 +287,12 @@ def update_cim_msg_analog(self, CIM_msg, index, value, conversion, model): print(str(CIM_units) +' not in model') return + # print(CIM_phase, CIM_units) + # print(model[CIM_units]) + if CIM_phase not in model[CIM_units]: + print(str(CIM_units) +' phase not correct in model') + return + mrid = model[CIM_units][CIM_phase]['mrid'] if type(multiplier) == str: multiplier = 1 @@ -375,6 +383,8 @@ def Process(self, info, values): # 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} # value_type = element_attr_to_mrid[dnp3_attr_name]['type'] # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier + elif str(index) in conversion['Analog input']: + self.update_cim_msg_analog(self._cim_msg, str(index), value, conversion, model) else: print(" No entry for index " + str(index)) # print(cim_msg) @@ -392,7 +402,7 @@ def Process(self, info, values): _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) # self._cim_msg = cim_msg - # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_msg)) + self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) # self._cim_msg = {"test":time.time()} print(self._cim_msg) diff --git a/test/master_cmd.py b/test/master_cmd.py index f2405a3..0c5ec39 100644 --- a/test/master_cmd.py +++ b/test/master_cmd.py @@ -30,12 +30,12 @@ class MasterCmd(cmd.Cmd): def __init__(self): cmd.Cmd.__init__(self) self.prompt = 'master> ' # Used by the Cmd framework, displayed when issuing a command-line prompt. - dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json") + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") elements_to_device = {'632633': 'Shark'} - self.application = MyMaster(HOST="127.0.0.1",# "127.0.0.1 + self.application = MyMaster(HOST="10.79.91.42",# "127.0.0.1 LOCAL="0.0.0.0", - PORT=20000, - DNP3_ADDR=10, + PORT=20007, + DNP3_ADDR=43, log_handler=MyLogger(), listener=AppChannelListener(), soe_handler=SOEHandler('632633', 'Shark', dnp3_to_cim), diff --git a/test/master_main.py b/test/master_main.py index f4d371b..0c6660d 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -10,12 +10,13 @@ # def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): def run_master(device_ip_port_config_all, names): masters = [] - dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json") + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") for name in names: device_ip_port_dict = device_ip_port_config_all[name] HOST=device_ip_port_dict['ip'] PORT=device_ip_port_dict['port'] DNP3_ADDR= device_ip_port_dict['link_local_addr'] + LocalAddr= device_ip_port_dict['link_remote_addr'] convertion_type=device_ip_port_dict['conversion_type'] object_name=device_ip_port_dict['CIM object'] @@ -23,6 +24,7 @@ def run_master(device_ip_port_config_all, names): LOCAL="0.0.0.0", PORT=int(PORT), DNP3_ADDR=int(DNP3_ADDR), + LocalAddr=int(LocalAddr), log_handler=MyLogger(), listener=AppChannelListener(), soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), @@ -33,14 +35,14 @@ def run_master(device_ip_port_config_all, names): SLEEP_SECONDS = 1 time.sleep(SLEEP_SECONDS) - # group_variation = opendnp3.GroupVariationID(32, 2) + group_variation = opendnp3.GroupVariationID(32, 2) # time.sleep(SLEEP_SECONDS) # print('\nReading status 1') - # application_1.master.ScanRange(group_variation, 0, 12) + application_1.master.ScanRange(group_variation, 0, 12) # time.sleep(SLEEP_SECONDS) # print('\nReading status 2') - # application_1.master.ScanRange(opendnp3.GroupVariationID(32, 2), 0, 3, opendnp3.TaskConfig().Default()) - # time.sleep(SLEEP_SECONDS) + application_1.master.ScanRange(opendnp3.GroupVariationID(32, 2), 0, 3, opendnp3.TaskConfig().Default()) + time.sleep(SLEEP_SECONDS) print('\nReading status 3') # application_1.slow_scan.Demand() From 71e77b61f08104cb2d3569fc4b47aea2a42f8035 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 16 Feb 2021 16:13:16 -0700 Subject: [PATCH 38/64] Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- dnp3/service/dnp3/dnp3_to_cim.py | 44 ++++++++++++++++++++++---------- dnp3/service/dnp3/master.py | 29 ++++++++++++++++++++- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/dnp3/service/dnp3/dnp3_to_cim.py b/dnp3/service/dnp3/dnp3_to_cim.py index 4eb66ac..17ec6b8 100644 --- a/dnp3/service/dnp3/dnp3_to_cim.py +++ b/dnp3/service/dnp3/dnp3_to_cim.py @@ -5,30 +5,36 @@ def get_conversion_model(csv_file,sheet_name): df = pd.read_excel(csv_file,sheet_name=sheet_name) df = df.replace(np.nan, '', regex=True) - master_dict = { - 'Analog input': {}, - 'Analog output': {}, - 'Binary input': {}, - 'Binary output': {} - } + master_dict ={ + 'Analog input':{}, + 'Analog output':{}, + 'Binary input':{}, + 'Binary output':{} } x = [] - ## Check for row breaks where there is no value or NaN for the 'Multipler' + for index, row in df.iterrows(): +# print(pd.isna(row['Multiplier'])) if pd.isna(row['Multiplier']) or row['Multiplier'] == '': - #print(index) +# print(index) x.append(index) -# print(df.shape) + print(df.shape) x.append(df.shape[0]) it = iter(x) for x in it: +# print(x) type_name = df.iloc[x][1] print(type_name) next_value = next(it) - print(x+1, next_value) + print (x+1, next_value) print(pd.DataFrame(df[x+1:next_value])) temp_df = pd.DataFrame(df[x+1: next_value]) - temp_df = temp_df.set_index('Index') - master_dict[type_name] = temp_df.T.to_dict() +# temp_df = temp_df.set_index('Index') +# temp_df = temp_df.replace(np.nan, '', regex=True) + temp_dict = temp_df.T.to_dict() + temp_dict_set_key_to_index = {} + for k,v in temp_dict.items(): + temp_dict_set_key_to_index[int(v['Index'])] = v + master_dict[type_name] = temp_dict_set_key_to_index return master_dict def build_conversion(csv_file): @@ -64,7 +70,19 @@ def get_device_dict(model_dict, model_line_dict, device_type, name): if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} - elif device_type == 'Beckwith CapBank 2': + elif device_type == 'RTU': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('EnergyConsumer_'+name): + # print(meas) + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + elif device_type == 'Beckwith CapBank': #LinearShuntCompensator for meas in model_dict['feeders'][0]['measurements']: if meas['name'].startswith('LinearShuntCompensator_'+name): diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 85691b6..c7e2d66 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -276,6 +276,30 @@ def __init__(self, name, device, dnp3_to_cim): def get_msg(self): return self._cim_msg + def update_cim_msg_analog_multi_index(self, CIM_msg, index, value, conversion, model): + CIM_phase = conversion[index]['CIM phase'] + CIM_units = conversion[index]['CIM units'] + CIM_attribute = conversion[index]['CIM attribute'] + ## Check if multiplier is na or str + multiplier = conversion[index]['Multiplier'] + if CIM_units not in model: + print(str(CIM_units) + ' not in model') + return + if CIM_phase not in model[CIM_units]: + print(str(model) + ' phase not correct in model', CIM_phase, CIM_units) + return + mrid = model[CIM_units][CIM_phase]['mrid'] + if type(multiplier) == str: + multiplier = 1 + + CIM_value = {'mrid': mrid, 'angle': 0} + # if CIM_units == 'PNV' or CIM_units == 'VA': + # CIM_value = {'mrid':mrid, 'magnitude':0,'angle':0} + + if mrid not in CIM_msg: + CIM_msg[mrid] = CIM_value + CIM_msg[mrid][CIM_attribute] = value * multiplier # times multipier + def update_cim_msg_analog(self, CIM_msg, index, value, conversion, model): if 'Analog input' in conversion and index in conversion['Analog input']: CIM_phase = conversion['Analog input'][index]['CIM phase'] @@ -363,7 +387,10 @@ def Process(self, info, values): print("Jeff SOE", index, value, isinstance(value, numbers.Number)) log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - if isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: + if 'RTU' in self._device: ##TODO Test! + conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} + self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) + elif isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) # ## Check if multiplier is na or str # multiplier = 1 From 00e4e03527303d26947311d1fbd4444be9b19617 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 26 Feb 2021 10:31:38 -0700 Subject: [PATCH 39/64] Updated to add master_dict. Fixed input checking. Fix mulit part Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- dnp3/service/dnp3/master.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index c7e2d66..10a6474 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -116,7 +116,7 @@ def __init__(self, self.stack_config.link.RemoteAddr = 1024 ## TODO get from config Was 10 self.stack_config.link.RemoteAddr = DNP3_ADDR self.stack_config.link.LocalAddr = LocalAddr - print('') + print(self.stack_config.link.RemoteAddr, self.stack_config.link.LocalAddr) _log.debug('Adding the master to the channel.') self.soe_handler = soe_handler @@ -382,13 +382,16 @@ def Process(self, info, values): ## TODO check for each type seperate binary vs analog ## Analog check + print(type(values)) if type(values) == opendnp3.ICollectionIndexedAnalog: for index, value in visitor.index_and_value: print("Jeff SOE", index, value, isinstance(value, numbers.Number)) - log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) if 'RTU' in self._device: ##TODO Test! + print('Jeff RTU') conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} + model = model_line_dict[conversion_name_index_dict[index]['CIM name']] self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) elif isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) @@ -419,14 +422,14 @@ def Process(self, info, values): elif type(values) == opendnp3.ICollectionIndexedBinaryOutputStatus: for index, value in visitor.index_and_value: print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) - log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work else: for index, value in visitor.index_and_value: print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) - log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) # self._cim_msg = cim_msg self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) @@ -480,6 +483,7 @@ def collection_callback(result=None): opendnp3.CommandPointStateToString(result.state), opendnp3.CommandStatusToString(result.status) )) + print(result.__doc__) def command_callback(result=None): From e8ebb406579fe17eac134e1b8df7c22e39a5e8cc Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 26 Feb 2021 11:52:33 -0700 Subject: [PATCH 40/64] Update to add csv --- dnp3/service/dnp3/master.py | 1048 ++++++++++++++++++----------------- test/master_main.py | 217 ++++---- 2 files changed, 648 insertions(+), 617 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 10a6474..63e1b85 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -1,519 +1,529 @@ -# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2018, SLAC / 8minutenergy / Kisensum. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared in part as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor SLAC, nor 8minutenergy, nor Kisensum, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# SLAC, 8minutenergy, or Kisensum. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# }}} - -import logging -import numbers -import sys -import time -import yaml -import json - -from gridappsd.topics import simulation_output_topic, simulation_input_topic -from gridappsd import GridAPPSD, DifferenceBuilder, utils - -from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 -from dnp3.visitors import * - -FILTERS = opendnp3.levels.NORMAL | opendnp3.levels.ALL_COMMS -FILTERS = opendnp3.levels.NOTHING -# HOST = "127.0.0.1" -# HOST = "192.168.1.2" -# LOCAL = "0.0.0.0" -# # LOCAL = "192.168.1.2" -# PORT = 20000 - - -stdout_stream = logging.StreamHandler(sys.stdout) -stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) - -_log = logging.getLogger(__name__) -_log.addHandler(stdout_stream) -_log.setLevel(logging.DEBUG) - - -class MyMaster: - """ - Interface for all master application callback info except for measurement values. - - DNP3 spec section 5.1.6.1: - The Application Layer provides the following services for the DNP3 User Layer in a master: - - Formats requests directed to one or more outstations. - - Notifies the DNP3 User Layer when new data or information arrives from an outstation. - - DNP spec section 5.1.6.3: - The Application Layer requires specific services from the layers beneath it. - - Partitioning of fragments into smaller portions for transport reliability. - - Knowledge of which device(s) were the source of received messages. - - Transmission of messages to specific devices or to all devices. - - Message integrity (i.e., error-free reception and transmission of messages). - - Knowledge of the time when messages arrive. - - Either precise times of transmission or the ability to set time values - into outgoing messages. - """ - def __init__(self, - HOST="192.168.1.2", # "127.0.0.1 - LOCAL= "0.0.0.0", - PORT=20000, - DNP3_ADDR=1, - LocalAddr=0, - log_handler=asiodnp3.ConsoleLogger().Create(), - listener=asiodnp3.PrintingChannelListener().Create(), - soe_handler=asiodnp3.PrintingSOEHandler().Create(), - master_application=asiodnp3.DefaultMasterApplication().Create(), - stack_config=None): - - _log.debug('Creating a DNP3Manager.') - self.log_handler = log_handler - self.manager = asiodnp3.DNP3Manager(1, self.log_handler) - - _log.debug('Creating the DNP3 channel, a TCP client.') - self.retry = asiopal.ChannelRetry().Default() - self.listener = listener - self.channel = self.manager.AddTCPClient("tcpclient", - FILTERS, - self.retry, - HOST, - LOCAL, - PORT, - self.listener) - - _log.debug('Configuring the DNP3 stack.') - self.stack_config = stack_config - if not self.stack_config: - self.stack_config = asiodnp3.MasterStackConfig() - self.stack_config.master.responseTimeout = openpal.TimeDuration().Seconds(2) - self.stack_config.link.RemoteAddr = 1024 ## TODO get from config Was 10 - self.stack_config.link.RemoteAddr = DNP3_ADDR - self.stack_config.link.LocalAddr = LocalAddr - print(self.stack_config.link.RemoteAddr, self.stack_config.link.LocalAddr) - - _log.debug('Adding the master to the channel.') - self.soe_handler = soe_handler - self.master_application = master_application - self.master = self.channel.AddMaster("master", - # asiodnp3.PrintingSOEHandler().Create(), - self.soe_handler, - self.master_application, - self.stack_config) - - _log.debug('Configuring some scans (periodic reads).') - # Set up a "slow scan", an infrequent integrity poll that requests events and static data for all classes. - self.slow_scan = self.master.AddClassScan(opendnp3.ClassField().AllClasses(), - openpal.TimeDuration().Minutes(30), - opendnp3.TaskConfig().Default()) - # Set up a "fast scan", a relatively-frequent exception poll that requests events and class 1 static data. - self.fast_scan = self.master.AddClassScan(opendnp3.ClassField(opendnp3.ClassField.CLASS_1), - openpal.TimeDuration().Minutes(1), - opendnp3.TaskConfig().Default()) - - self.fast_scan_all = self.master.AddClassScan(opendnp3.ClassField().AllClasses(), - openpal.TimeDuration().Minutes(1), - opendnp3.TaskConfig().Default()) - - # self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) - # self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) - self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.NOTHING)) - self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.NOTHING)) - - - _log.debug('Enabling the master. At this point, traffic will start to flow between the Master and Outstations.') - self.master.Enable() - time.sleep(5) - - @classmethod - def get_agent(cls): - """Return the singleton DNP3Agent """ - agt = cls.agent - if agt is None: - raise ValueError('Master has no configured agent') - return agt - - @classmethod - def set_agent(cls, agent): - """Set the singleton DNP3Agent """ - cls.agent = agent - - def send_direct_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Direct operate a single command - - :param command: command to operate - :param index: index of the command - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.DirectOperate(command, index, callback, config) - - def send_direct_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Direct operate a set of commands - - :param command_set: set of command headers - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.DirectOperate(command_set, callback, config) - - def send_select_and_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Select and operate a single command - - :param command: command to operate - :param index: index of the command - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.SelectAndOperate(command, index, callback, config) - - def send_select_and_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), - config=opendnp3.TaskConfig().Default()): - """ - Select and operate a set of commands - - :param command_set: set of command headers - :param callback: callback that will be invoked upon completion or failure - :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA - """ - self.master.SelectAndOperate(command_set, callback, config) - - def shutdown(self): - del self.slow_scan - del self.fast_scan - del self.master - del self.channel - self.manager.Shutdown() - - -class MyLogger(openpal.ILogHandler): - """ - Override ILogHandler in this manner to implement application-specific logging behavior. - """ - - def __init__(self): - super(MyLogger, self).__init__() - - def Log(self, entry): - flag = opendnp3.LogFlagToString(entry.filters.GetBitfield()) - filters = entry.filters.GetBitfield() - location = entry.location.rsplit('/')[-1] if entry.location else '' - message = entry.message - _log.debug('LOG\t\t{:<10}\tfilters={:<5}\tlocation={:<25}\tentry={}'.format(flag, filters, location, message)) - # pass - - -class AppChannelListener(asiodnp3.IChannelListener): - """ - Override IChannelListener in this manner to implement application-specific channel behavior. - """ - - def __init__(self): - super(AppChannelListener, self).__init__() - - def OnStateChange(self, state): - # _log.debug('In AppChannelListener.OnStateChange: state={}'.format(opendnp3.ChannelStateToString(state))) - pass - - - - -def on_message(self, message): - json_msg = yaml.safe_load(str(message)) - print(json_msg) - - -class SOEHandler(opendnp3.ISOEHandler): - """ - Override ISOEHandler in this manner to implement application-specific sequence-of-events behavior. - - This is an interface for SequenceOfEvents (SOE) callbacks from the Master stack to the application layer. - """ - - def __init__(self, name, device, dnp3_to_cim): - self._name = name - self._device = device - self._dnp3_to_cim = dnp3_to_cim - self._gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), - username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) - self._gapps.subscribe('/topic/goss.gridappsd.fim.input.'+str(1234), on_message) - self._cim_msg = {} - - super(SOEHandler, self).__init__() - - def get_msg(self): - return self._cim_msg - - def update_cim_msg_analog_multi_index(self, CIM_msg, index, value, conversion, model): - CIM_phase = conversion[index]['CIM phase'] - CIM_units = conversion[index]['CIM units'] - CIM_attribute = conversion[index]['CIM attribute'] - ## Check if multiplier is na or str - multiplier = conversion[index]['Multiplier'] - if CIM_units not in model: - print(str(CIM_units) + ' not in model') - return - if CIM_phase not in model[CIM_units]: - print(str(model) + ' phase not correct in model', CIM_phase, CIM_units) - return - mrid = model[CIM_units][CIM_phase]['mrid'] - if type(multiplier) == str: - multiplier = 1 - - CIM_value = {'mrid': mrid, 'angle': 0} - # if CIM_units == 'PNV' or CIM_units == 'VA': - # CIM_value = {'mrid':mrid, 'magnitude':0,'angle':0} - - if mrid not in CIM_msg: - CIM_msg[mrid] = CIM_value - CIM_msg[mrid][CIM_attribute] = value * multiplier # times multipier - - def update_cim_msg_analog(self, CIM_msg, index, value, conversion, model): - if 'Analog input' in conversion and index in conversion['Analog input']: - CIM_phase = conversion['Analog input'][index]['CIM phase'] - CIM_units = conversion['Analog input'][index]['CIM units'] - CIM_attribute = conversion['Analog input'][index]['CIM attribute'] - ## Check if multiplier is na or str - multiplier = conversion['Analog input'][index]['Multiplier'] - if CIM_units not in model: - print(str(CIM_units) +' not in model') - return - - # print(CIM_phase, CIM_units) - # print(model[CIM_units]) - if CIM_phase not in model[CIM_units]: - print(str(CIM_units) +' phase not correct in model') - return - - mrid = model[CIM_units][CIM_phase]['mrid'] - if type(multiplier) == str: - multiplier = 1 - - CIM_value = {'mrid': mrid} - if CIM_units == 'PNV' or CIM_units == 'VA': - CIM_value = {'mrid': mrid, 'magnitude': 0, 'angle': 0} - - if mrid not in CIM_msg: - CIM_msg[mrid] = CIM_value - CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier - - def update_cim_msg_binary(self, CIM_msg, index, value, conversion,model): - # print(conversion['Binary input'][index]) - if 'Binary input' in conversion and index in conversion['Binary input']: - CIM_phases = conversion['Binary input'][index]['CIM phase'] - CIM_units = conversion['Binary input'][index]['CIM units'] - CIM_attribute = conversion['Binary input'][index]['CIM attribute'] - ## Check if multiplier is na or str - multiplier = conversion['Binary input'][index]['Multiplier'] - print(type(CIM_units),CIM_units) - if CIM_units not in model: - print(str(CIM_units) +' not in model') - return - for CIM_phase in CIM_phases: - # print(model) - # exit(0) - mrid = model[CIM_units][CIM_phase]['mrid'] - CIM_value = {'mrid': mrid} - if mrid not in CIM_msg: - CIM_msg[mrid] = CIM_value - CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier - - def Process(self, info, values): - """ - Process measurement data. - - :param info: HeaderInfo - :param values: A collection of values received from the Outstation (various data types are possible). - """ - visitor_class_types = { - opendnp3.ICollectionIndexedBinary: VisitorIndexedBinary, - opendnp3.ICollectionIndexedDoubleBitBinary: VisitorIndexedDoubleBitBinary, - opendnp3.ICollectionIndexedCounter: VisitorIndexedCounter, - opendnp3.ICollectionIndexedFrozenCounter: VisitorIndexedFrozenCounter, - opendnp3.ICollectionIndexedAnalog: VisitorIndexedAnalog, - opendnp3.ICollectionIndexedBinaryOutputStatus: VisitorIndexedBinaryOutputStatus, - opendnp3.ICollectionIndexedAnalogOutputStatus: VisitorIndexedAnalogOutputStatus, - opendnp3.ICollectionIndexedTimeAndInterval: VisitorIndexedTimeAndInterval - } - visitor_class = visitor_class_types[type(values)] - visitor = visitor_class() - values.Foreach(visitor) - - # cim_msg = {} - conversion_dict = self._dnp3_to_cim.conversion_dict - model_line_dict = self._dnp3_to_cim.model_line_dict - - element_attr_to_mrid = model_line_dict[self._name] - model = model_line_dict[self._name] - conversion = conversion_dict[self._device] - # print(conversion) - - ## TODO check for each type seperate binary vs analog - ## Analog check - print(type(values)) - if type(values) == opendnp3.ICollectionIndexedAnalog: - for index, value in visitor.index_and_value: - print("Jeff SOE", index, value, isinstance(value, numbers.Number)) - # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - if 'RTU' in self._device: ##TODO Test! - print('Jeff RTU') - conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} - model = model_line_dict[conversion_name_index_dict[index]['CIM name']] - self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) - elif isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: - self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) - # ## Check if multiplier is na or str - # multiplier = 1 - # conversion_for_index = conversion['Analog input'][str(float(index))] - # if 'Multiplier' in conversion_for_index: - # multiplier = conversion_for_index['Multiplier'] - # if type(multiplier) != str: - # multiplier = conversion_for_index['Multiplier'] - # else: - # print("No multiplier") - # dnp3_attr_name = conversion_for_index['type'] - # if dnp3_attr_name in element_attr_to_mrid: - # # print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) - # if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in self._cim_msg: - # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, - # 'angle': 0, - # 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} - # value_type = element_attr_to_mrid[dnp3_attr_name]['type'] - # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier - elif str(index) in conversion['Analog input']: - self.update_cim_msg_analog(self._cim_msg, str(index), value, conversion, model) - else: - print(" No entry for index " + str(index)) - # print(cim_msg) - ## '/topic/goss.gridappsd.fim.input' - elif type(values) == opendnp3.ICollectionIndexedBinaryOutputStatus: - for index, value in visitor.index_and_value: - print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) - # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work - else: - for index, value in visitor.index_and_value: - print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) - # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - - # self._cim_msg = cim_msg - self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) - - # self._cim_msg = {"test":time.time()} - print(self._cim_msg) - def Start(self): - _log.debug('In SOEHandler.Start') - - def End(self): - _log.debug('In SOEHandler.End') - - -class MasterApplication(opendnp3.IMasterApplication): - def __init__(self): - super(MasterApplication, self).__init__() - - # Overridden method - def AssignClassDuringStartup(self): - _log.debug('In MasterApplication.AssignClassDuringStartup') - return False - - # Overridden method - def OnClose(self): - _log.debug('In MasterApplication.OnClose') - - # Overridden method - def OnOpen(self): - _log.debug('In MasterApplication.OnOpen') - - # Overridden method - def OnReceiveIIN(self, iin): - _log.debug('In MasterApplication.OnReceiveIIN') - - # Overridden method - def OnTaskComplete(self, info): - _log.debug('In MasterApplication.OnTaskComplete') - - # Overridden method - def OnTaskStart(self, type, id): - _log.debug('In MasterApplication.OnTaskStart') - - -def collection_callback(result=None): - """ - :type result: opendnp3.CommandPointResult - """ - print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( - result.headerIndex, - result.index, - opendnp3.CommandPointStateToString(result.state), - opendnp3.CommandStatusToString(result.status) - )) - print(result.__doc__) - - -def command_callback(result=None): - """ - :type result: opendnp3.ICommandTaskResult - """ - print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) - result.ForeachItem(collection_callback) - - -def restart_callback(result=opendnp3.RestartOperationResult()): - if result.summary == opendnp3.TaskCompletion.SUCCESS: - print("Restart success | Restart Time: {}".format(result.restartTime.GetMilliseconds())) - else: - print("Restart fail | Failure: {}".format(opendnp3.TaskCompletionToString(result.summary))) - - -def main(): - """The Master has been started from the command line. Execute ad-hoc tests if desired.""" - # app = MyMaster() - app = MyMaster(log_handler=MyLogger(), - listener=AppChannelListener(), - soe_handler=SOEHandler(), - master_application=MasterApplication()) - _log.debug('Initialization complete. In command loop.') - # Ad-hoc tests can be performed at this point. See master_cmd.py for examples. - app.shutdown() - _log.debug('Exiting.') - exit() - - -if __name__ == '__main__': - main() +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2018, SLAC / 8minutenergy / Kisensum. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared in part as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor SLAC, nor 8minutenergy, nor Kisensum, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# SLAC, 8minutenergy, or Kisensum. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# }}} + +import logging +import numbers +import sys +import time +import yaml +import json +import numpy as np + +from gridappsd.topics import simulation_output_topic, simulation_input_topic +from gridappsd import GridAPPSD, DifferenceBuilder, utils + +from pydnp3 import opendnp3, openpal, asiopal, asiodnp3 +from dnp3.visitors import * + +FILTERS = opendnp3.levels.NORMAL | opendnp3.levels.ALL_COMMS +FILTERS = opendnp3.levels.NOTHING +# HOST = "127.0.0.1" +# HOST = "192.168.1.2" +# LOCAL = "0.0.0.0" +# # LOCAL = "192.168.1.2" +# PORT = 20000 + + +stdout_stream = logging.StreamHandler(sys.stdout) +stdout_stream.setFormatter(logging.Formatter('%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s')) + +_log = logging.getLogger(__name__) +_log.addHandler(stdout_stream) +_log.setLevel(logging.DEBUG) + + +class MyMaster: + """ + Interface for all master application callback info except for measurement values. + + DNP3 spec section 5.1.6.1: + The Application Layer provides the following services for the DNP3 User Layer in a master: + - Formats requests directed to one or more outstations. + - Notifies the DNP3 User Layer when new data or information arrives from an outstation. + + DNP spec section 5.1.6.3: + The Application Layer requires specific services from the layers beneath it. + - Partitioning of fragments into smaller portions for transport reliability. + - Knowledge of which device(s) were the source of received messages. + - Transmission of messages to specific devices or to all devices. + - Message integrity (i.e., error-free reception and transmission of messages). + - Knowledge of the time when messages arrive. + - Either precise times of transmission or the ability to set time values + into outgoing messages. + """ + def __init__(self, + HOST="192.168.1.2", # "127.0.0.1 + LOCAL= "0.0.0.0", + PORT=20000, + DNP3_ADDR=1, + LocalAddr=0, + log_handler=asiodnp3.ConsoleLogger().Create(), + listener=asiodnp3.PrintingChannelListener().Create(), + soe_handler=asiodnp3.PrintingSOEHandler().Create(), + master_application=asiodnp3.DefaultMasterApplication().Create(), + stack_config=None): + + _log.debug('Creating a DNP3Manager.') + self.log_handler = log_handler + self.manager = asiodnp3.DNP3Manager(1, self.log_handler) + + _log.debug('Creating the DNP3 channel, a TCP client.') + self.retry = asiopal.ChannelRetry().Default() + self.listener = listener + self.channel = self.manager.AddTCPClient("tcpclient", + FILTERS, + self.retry, + HOST, + LOCAL, + PORT, + self.listener) + + _log.debug('Configuring the DNP3 stack.') + self.stack_config = stack_config + if not self.stack_config: + self.stack_config = asiodnp3.MasterStackConfig() + self.stack_config.master.responseTimeout = openpal.TimeDuration().Seconds(2) + self.stack_config.link.RemoteAddr = 1024 ## TODO get from config Was 10 + self.stack_config.link.RemoteAddr = DNP3_ADDR + self.stack_config.link.LocalAddr = LocalAddr + print(self.stack_config.link.RemoteAddr, self.stack_config.link.LocalAddr) + + _log.debug('Adding the master to the channel.') + self.soe_handler = soe_handler + self.master_application = master_application + self.master = self.channel.AddMaster("master", + # asiodnp3.PrintingSOEHandler().Create(), + self.soe_handler, + self.master_application, + self.stack_config) + + _log.debug('Configuring some scans (periodic reads).') + # Set up a "slow scan", an infrequent integrity poll that requests events and static data for all classes. + self.slow_scan = self.master.AddClassScan(opendnp3.ClassField().AllClasses(), + openpal.TimeDuration().Minutes(30), + opendnp3.TaskConfig().Default()) + # Set up a "fast scan", a relatively-frequent exception poll that requests events and class 1 static data. + self.fast_scan = self.master.AddClassScan(opendnp3.ClassField(opendnp3.ClassField.CLASS_1), + openpal.TimeDuration().Minutes(1), + opendnp3.TaskConfig().Default()) + + self.fast_scan_all = self.master.AddClassScan(opendnp3.ClassField().AllClasses(), + openpal.TimeDuration().Minutes(1), + opendnp3.TaskConfig().Default()) + + # self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + # self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + self.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.NOTHING)) + self.master.SetLogFilters(openpal.LogFilters(opendnp3.levels.NOTHING)) + + + _log.debug('Enabling the master. At this point, traffic will start to flow between the Master and Outstations.') + self.master.Enable() + time.sleep(5) + + @classmethod + def get_agent(cls): + """Return the singleton DNP3Agent """ + agt = cls.agent + if agt is None: + raise ValueError('Master has no configured agent') + return agt + + @classmethod + def set_agent(cls, agent): + """Set the singleton DNP3Agent """ + cls.agent = agent + + def send_direct_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Direct operate a single command + + :param command: command to operate + :param index: index of the command + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.DirectOperate(command, index, callback, config) + + def send_direct_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Direct operate a set of commands + + :param command_set: set of command headers + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.DirectOperate(command_set, callback, config) + + def send_select_and_operate_command(self, command, index, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Select and operate a single command + + :param command: command to operate + :param index: index of the command + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.SelectAndOperate(command, index, callback, config) + + def send_select_and_operate_command_set(self, command_set, callback=asiodnp3.PrintingCommandCallback.Get(), + config=opendnp3.TaskConfig().Default()): + """ + Select and operate a set of commands + + :param command_set: set of command headers + :param callback: callback that will be invoked upon completion or failure + :param config: optional configuration that controls normal callbacks and allows the user to be specified for SA + """ + self.master.SelectAndOperate(command_set, callback, config) + + def shutdown(self): + del self.slow_scan + del self.fast_scan + del self.master + del self.channel + self.manager.Shutdown() + + +class MyLogger(openpal.ILogHandler): + """ + Override ILogHandler in this manner to implement application-specific logging behavior. + """ + + def __init__(self): + super(MyLogger, self).__init__() + + def Log(self, entry): + flag = opendnp3.LogFlagToString(entry.filters.GetBitfield()) + filters = entry.filters.GetBitfield() + location = entry.location.rsplit('/')[-1] if entry.location else '' + message = entry.message + _log.debug('LOG\t\t{:<10}\tfilters={:<5}\tlocation={:<25}\tentry={}'.format(flag, filters, location, message)) + # pass + + +class AppChannelListener(asiodnp3.IChannelListener): + """ + Override IChannelListener in this manner to implement application-specific channel behavior. + """ + + def __init__(self): + super(AppChannelListener, self).__init__() + + def OnStateChange(self, state): + # _log.debug('In AppChannelListener.OnStateChange: state={}'.format(opendnp3.ChannelStateToString(state))) + pass + + + + +def on_message(self, message): + json_msg = yaml.safe_load(str(message)) + print(json_msg) + + +class SOEHandler(opendnp3.ISOEHandler): + """ + Override ISOEHandler in this manner to implement application-specific sequence-of-events behavior. + + This is an interface for SequenceOfEvents (SOE) callbacks from the Master stack to the application layer. + """ + + def __init__(self, name, device, dnp3_to_cim): + self._name = name + self._device = device + self._dnp3_to_cim = dnp3_to_cim + self._gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), + username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + self._gapps.subscribe('/topic/goss.gridappsd.fim.input.'+str(1234), on_message) + self._cim_msg = {} + self._dnp3_msg = {} + + super(SOEHandler, self).__init__() + + def get_msg(self): + return self._cim_msg + + def get_dnp3_msg(self): + return self._dnp3_msg + + def update_cim_msg_analog_multi_index(self, CIM_msg, index, value, conversion, model): + CIM_phase = conversion[index]['CIM phase'] + CIM_units = conversion[index]['CIM units'] + CIM_attribute = conversion[index]['CIM attribute'] + ## Check if multiplier is na or str + multiplier = conversion[index]['Multiplier'] + if CIM_units not in model: + print(str(CIM_units) + ' not in model') + return + if CIM_phase not in model[CIM_units]: + print(str(model) + ' phase not correct in model', CIM_phase, CIM_units) + return + mrid = model[CIM_units][CIM_phase]['mrid'] + if type(multiplier) == str: + multiplier = 1 + + CIM_value = {'mrid': mrid, 'angle': 0} + # if CIM_units == 'PNV' or CIM_units == 'VA': + # CIM_value = {'mrid':mrid, 'magnitude':0,'angle':0} + + if mrid not in CIM_msg: + CIM_msg[mrid] = CIM_value + CIM_msg[mrid][CIM_attribute] = value * multiplier # times multipier + + def update_cim_msg_analog(self, CIM_msg, index, value, conversion, model): + if 'Analog input' in conversion and index in conversion['Analog input']: + CIM_phase = conversion['Analog input'][index]['CIM phase'] + CIM_units = conversion['Analog input'][index]['CIM units'] + CIM_attribute = conversion['Analog input'][index]['CIM attribute'] + ## Check if multiplier is na or str + multiplier = conversion['Analog input'][index]['Multiplier'] + if CIM_units not in model: + print(str(CIM_units) +' not in model') + return + + # print(CIM_phase, CIM_units) + # print(model[CIM_units]) + if CIM_phase not in model[CIM_units]: + print(str(CIM_units) +' phase not correct in model') + return + + mrid = model[CIM_units][CIM_phase]['mrid'] + if type(multiplier) == str: + multiplier = 1 + + CIM_value = {'mrid': mrid} + if CIM_units == 'PNV' or CIM_units == 'VA': + CIM_value = {'mrid': mrid, 'magnitude': 0, 'angle': 0} + + if mrid not in CIM_msg: + CIM_msg[mrid] = CIM_value + CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier + + def update_cim_msg_binary(self, CIM_msg, index, value, conversion,model): + # print(conversion['Binary input'][index]) + if 'Binary input' in conversion and index in conversion['Binary input']: + CIM_phases = conversion['Binary input'][index]['CIM phase'] + CIM_units = conversion['Binary input'][index]['CIM units'] + CIM_attribute = conversion['Binary input'][index]['CIM attribute'] + ## Check if multiplier is na or str + multiplier = conversion['Binary input'][index]['Multiplier'] + print(type(CIM_units),CIM_units) + if CIM_units not in model: + print(str(CIM_units) +' not in model') + return + for CIM_phase in CIM_phases: + # print(model) + # exit(0) + mrid = model[CIM_units][CIM_phase]['mrid'] + CIM_value = {'mrid': mrid} + if mrid not in CIM_msg: + CIM_msg[mrid] = CIM_value + CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier + + def Process(self, info, values): + """ + Process measurement data. + + :param info: HeaderInfo + :param values: A collection of values received from the Outstation (various data types are possible). + """ + visitor_class_types = { + opendnp3.ICollectionIndexedBinary: VisitorIndexedBinary, + opendnp3.ICollectionIndexedDoubleBitBinary: VisitorIndexedDoubleBitBinary, + opendnp3.ICollectionIndexedCounter: VisitorIndexedCounter, + opendnp3.ICollectionIndexedFrozenCounter: VisitorIndexedFrozenCounter, + opendnp3.ICollectionIndexedAnalog: VisitorIndexedAnalog, + opendnp3.ICollectionIndexedBinaryOutputStatus: VisitorIndexedBinaryOutputStatus, + opendnp3.ICollectionIndexedAnalogOutputStatus: VisitorIndexedAnalogOutputStatus, + opendnp3.ICollectionIndexedTimeAndInterval: VisitorIndexedTimeAndInterval + } + visitor_class = visitor_class_types[type(values)] + visitor = visitor_class() + values.Foreach(visitor) + + # cim_msg = {} + conversion_dict = self._dnp3_to_cim.conversion_dict + model_line_dict = self._dnp3_to_cim.model_line_dict + + element_attr_to_mrid = model_line_dict[self._name] + model = model_line_dict[self._name] + conversion = conversion_dict[self._device] + print(conversion) + + ## TODO check for each type seperate binary vs analog + ## Analog check + print(type(values)) + if type(values) == opendnp3.ICollectionIndexedAnalog: + for index, value in visitor.index_and_value: + print("Jeff SOE", index, value, isinstance(value, numbers.Number)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + if 'RTU' in self._device: + print('Jeff RTU') + self._dnp3_msg[index]=value + conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} + if index in conversion_name_index_dict: + # _log.debug("Conversion for " + str(index)) + model = model_line_dict[conversion_name_index_dict[index]['CIM name']] + self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) + else: + _log.debug("No conversion for " + str(index)) + elif isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: + self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) + # ## Check if multiplier is na or str + # multiplier = 1 + # conversion_for_index = conversion['Analog input'][str(float(index))] + # if 'Multiplier' in conversion_for_index: + # multiplier = conversion_for_index['Multiplier'] + # if type(multiplier) != str: + # multiplier = conversion_for_index['Multiplier'] + # else: + # print("No multiplier") + # dnp3_attr_name = conversion_for_index['type'] + # if dnp3_attr_name in element_attr_to_mrid: + # # print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) + # if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in self._cim_msg: + # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, + # 'angle': 0, + # 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} + # value_type = element_attr_to_mrid[dnp3_attr_name]['type'] + # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier + elif str(index) in conversion['Analog input']: + self.update_cim_msg_analog(self._cim_msg, str(index), value, conversion, model) + else: + print(" No entry for index " + str(index)) + # print(cim_msg) + ## '/topic/goss.gridappsd.fim.input' + elif type(values) == opendnp3.ICollectionIndexedBinaryOutputStatus: + for index, value in visitor.index_and_value: + print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work + else: + for index, value in visitor.index_and_value: + print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + + # self._cim_msg = cim_msg + self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) + + # self._cim_msg = {"test":time.time()} + print(self._cim_msg) + def Start(self): + _log.debug('In SOEHandler.Start') + + def End(self): + _log.debug('In SOEHandler.End') + + +class MasterApplication(opendnp3.IMasterApplication): + def __init__(self): + super(MasterApplication, self).__init__() + + # Overridden method + def AssignClassDuringStartup(self): + _log.debug('In MasterApplication.AssignClassDuringStartup') + return False + + # Overridden method + def OnClose(self): + _log.debug('In MasterApplication.OnClose') + + # Overridden method + def OnOpen(self): + _log.debug('In MasterApplication.OnOpen') + + # Overridden method + def OnReceiveIIN(self, iin): + _log.debug('In MasterApplication.OnReceiveIIN') + + # Overridden method + def OnTaskComplete(self, info): + _log.debug('In MasterApplication.OnTaskComplete') + + # Overridden method + def OnTaskStart(self, type, id): + _log.debug('In MasterApplication.OnTaskStart') + + +def collection_callback(result=None): + """ + :type result: opendnp3.CommandPointResult + """ + print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( + result.headerIndex, + result.index, + opendnp3.CommandPointStateToString(result.state), + opendnp3.CommandStatusToString(result.status) + )) + print(result.__doc__) + + +def command_callback(result=None): + """ + :type result: opendnp3.ICommandTaskResult + """ + print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) + result.ForeachItem(collection_callback) + + +def restart_callback(result=opendnp3.RestartOperationResult()): + if result.summary == opendnp3.TaskCompletion.SUCCESS: + print("Restart success | Restart Time: {}".format(result.restartTime.GetMilliseconds())) + else: + print("Restart fail | Failure: {}".format(opendnp3.TaskCompletionToString(result.summary))) + + +def main(): + """The Master has been started from the command line. Execute ad-hoc tests if desired.""" + # app = MyMaster() + app = MyMaster(log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(), + master_application=MasterApplication()) + _log.debug('Initialization complete. In command loop.') + # Ad-hoc tests can be performed at this point. See master_cmd.py for examples. + app.shutdown() + _log.debug('Exiting.') + exit() + + +if __name__ == '__main__': + main() diff --git a/test/master_main.py b/test/master_main.py index 0c6660d..7faf2d6 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -1,99 +1,120 @@ -import os -import sys -import time -sys.path.append("../dnp3/service") - -from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication -from dnp3.dnp3_to_cim import CIMMapping -from pydnp3 import opendnp3, openpal - -# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): -def run_master(device_ip_port_config_all, names): - masters = [] - dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") - for name in names: - device_ip_port_dict = device_ip_port_config_all[name] - HOST=device_ip_port_dict['ip'] - PORT=device_ip_port_dict['port'] - DNP3_ADDR= device_ip_port_dict['link_local_addr'] - LocalAddr= device_ip_port_dict['link_remote_addr'] - convertion_type=device_ip_port_dict['conversion_type'] - object_name=device_ip_port_dict['CIM object'] - - application_1 = MyMaster(HOST=HOST, # "127.0.0.1 - LOCAL="0.0.0.0", - PORT=int(PORT), - DNP3_ADDR=int(DNP3_ADDR), - LocalAddr=int(LocalAddr), - log_handler=MyLogger(), - listener=AppChannelListener(), - soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), - master_application=MasterApplication()) - # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) - # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) - masters.append(application_1) - - SLEEP_SECONDS = 1 - time.sleep(SLEEP_SECONDS) - group_variation = opendnp3.GroupVariationID(32, 2) - # time.sleep(SLEEP_SECONDS) - # print('\nReading status 1') - application_1.master.ScanRange(group_variation, 0, 12) - # time.sleep(SLEEP_SECONDS) - # print('\nReading status 2') - application_1.master.ScanRange(opendnp3.GroupVariationID(32, 2), 0, 3, opendnp3.TaskConfig().Default()) - time.sleep(SLEEP_SECONDS) - print('\nReading status 3') - # application_1.slow_scan.Demand() - - # application_1.fast_scan_all.Demand() - - # for master in masters: - # master.fast_scan_all.Demand() - while True: - cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} - for master in masters: - cim_msg = master.soe_handler.get_msg() - # print(cim_msg) - cim_full_msg['messages'].update(cim_msg) - - print(cim_full_msg) - time.sleep(10) - - - print('\nStopping') - for master in masters: - master.shutdown() - # application_1.shutdown() - exit() - - # When terminating, it is necessary to set these to None so that - # it releases the shared pointer. Otherwise, python will not - # terminate (and even worse, the normal Ctrl+C won't help). - application_1.master.Disable() - application_1 = None - application_1.channel.Shutdown() - application_1.channel = None - application_1.manager.Shutdown() - -if __name__ == "__main__": - - import argparse - import json - parser = argparse.ArgumentParser() - parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) - # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') - args = parser.parse_args() - print(args.names) - # exit(0) - with open("device_ip_port_config_all.json") as f: - device_ip_port_config_all = json.load(f) - - device_ip_port_dict = device_ip_port_config_all[args.names[0]] - print(device_ip_port_dict) - run_master(device_ip_port_config_all, args.names) - # run_master(device_ip_port_dict['ip'], - # device_ip_port_dict['port'], - # device_ip_port_dict['link_local_addr'], - # device_ip_port_dict['conversion_type'], +import os +import sys +import time +import csv +import numpy as np +sys.path.append("../dnp3/service") + +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.dnp3_to_cim import CIMMapping +from pydnp3 import opendnp3, openpal + + +def build_csv_writers(folder, filename, column_names): + _file = os.path.join(folder, filename) + if os.path.exists(_file): + os.remove(_file) + file_handle = open(_file, 'a') + csv_writer = csv.writer(file_handle, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) + csv_writer.writerow(column_names) + # csv_writer.writerow(['epoch time'] + column_names) + return file_handle, csv_writer + +# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): +def run_master(device_ip_port_config_all, names): + masters = [] + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") + for name in names: + device_ip_port_dict = device_ip_port_config_all[name] + HOST=device_ip_port_dict['ip'] + PORT=device_ip_port_dict['port'] + DNP3_ADDR= device_ip_port_dict['link_local_addr'] + LocalAddr= device_ip_port_dict['link_remote_addr'] + convertion_type=device_ip_port_dict['conversion_type'] + object_name=device_ip_port_dict['CIM object'] + + application_1 = MyMaster(HOST=HOST, # "127.0.0.1 + LOCAL="0.0.0.0", + PORT=int(PORT), + DNP3_ADDR=int(DNP3_ADDR), + LocalAddr=int(LocalAddr), + log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), + master_application=MasterApplication()) + # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) + masters.append(application_1) + + SLEEP_SECONDS = 1 + time.sleep(SLEEP_SECONDS) + group_variation = opendnp3.GroupVariationID(32, 2) + # time.sleep(SLEEP_SECONDS) + # print('\nReading status 1') + application_1.master.ScanRange(group_variation, 0, 12) + # time.sleep(SLEEP_SECONDS) + # print('\nReading status 2') + application_1.master.ScanRange(opendnp3.GroupVariationID(32, 2), 0, 3, opendnp3.TaskConfig().Default()) + time.sleep(SLEEP_SECONDS) + print('\nReading status 3') + # application_1.slow_scan.Demand() + + # application_1.fast_scan_all.Demand() + + # for master in masters: + # master.fast_scan_all.Demand() + msg_count=0 + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', 'rtu_7.csv', list(range(300))) + while True: + cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} + for master in masters: + cim_msg = master.soe_handler.get_msg() + dnp3_msg = master.soe_handler.get_dnp3_msg() + max_index = max(dnp3_msg.keys()) + values = [dnp3_msg[k] for k in range(max_index)] + rtu_7_writer.writerow(np.insert(values,0, msg_count)) + rtu_7_csvfile.flush() + # print(cim_msg) + cim_full_msg['messages'].update(cim_msg) + msg_count+=1 + + print(cim_full_msg) + time.sleep(30) + + + print('\nStopping') + for master in masters: + master.shutdown() + # application_1.shutdown() + exit() + + # When terminating, it is necessary to set these to None so that + # it releases the shared pointer. Otherwise, python will not + # terminate (and even worse, the normal Ctrl+C won't help). + application_1.master.Disable() + application_1 = None + application_1.channel.Shutdown() + application_1.channel = None + application_1.manager.Shutdown() + +if __name__ == "__main__": + + import argparse + import json + parser = argparse.ArgumentParser() + parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) + # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') + args = parser.parse_args() + print(args.names) + # exit(0) + with open("device_ip_port_config_all.json") as f: + device_ip_port_config_all = json.load(f) + + device_ip_port_dict = device_ip_port_config_all[args.names[0]] + print(device_ip_port_dict) + run_master(device_ip_port_config_all, args.names) + # run_master(device_ip_port_dict['ip'], + # device_ip_port_dict['port'], + # device_ip_port_dict['link_local_addr'], + # device_ip_port_dict['conversion_type'], # '632633') \ No newline at end of file From 2c547e1d7ec119b55d46a64979f9cea650abb216 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 2 Mar 2021 11:35:31 -0700 Subject: [PATCH 41/64] Added send cmd --- test/README.md | 5 ++ test/master_send_cmd.py | 180 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 test/master_send_cmd.py diff --git a/test/README.md b/test/README.md index 296ad3c..2b469ad 100644 --- a/test/README.md +++ b/test/README.md @@ -25,3 +25,8 @@ python outstation_main.py 'test outstation 1' TODO Alka Complete integration of capbank in new_start_service and test with stand alone outstation_cmd or outstation_main. + +Command line send cmd to RTU_7 +``` +(dnp3_env) gridappsd@gridappsd-VirtualBox:/media/sf_git_scada_read/gridappsd-dnp3/test$ python master_send_cmd.py 'RTU_7' +``` diff --git a/test/master_send_cmd.py b/test/master_send_cmd.py new file mode 100644 index 0000000..0f155aa --- /dev/null +++ b/test/master_send_cmd.py @@ -0,0 +1,180 @@ +import os +import sys +import time +sys.path.append("../dnp3/service") + +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.dnp3_to_cim import CIMMapping +from pydnp3 import opendnp3, openpal + + + + +def collection_callback(result=None): + """ + :type result: opendnp3.CommandPointResult + """ + print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( + result.headerIndex, + result.index, + opendnp3.CommandPointStateToString(result.state), + opendnp3.CommandStatusToString(result.status) + )) + # print(result) + + +def command_callback(result=None): + """ + :type result: opendnp3.ICommandTaskResult + """ + print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) + result.ForeachItem(collection_callback) + + + +# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): +def run_master(device_ip_port_config_all, names): + masters = [] + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") + for name in names: + device_ip_port_dict = device_ip_port_config_all[name] + HOST=device_ip_port_dict['ip'] + PORT=device_ip_port_dict['port'] + DNP3_ADDR= device_ip_port_dict['link_local_addr'] + LocalAddr= device_ip_port_dict['link_remote_addr'] + convertion_type=device_ip_port_dict['conversion_type'] + object_name=device_ip_port_dict['CIM object'] + + application_1 = MyMaster(HOST=HOST, # "127.0.0.1 + LOCAL="0.0.0.0", + PORT=int(PORT), + DNP3_ADDR=int(DNP3_ADDR), + LocalAddr=int(LocalAddr), + log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), + master_application=MasterApplication()) + # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) + # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) + masters.append(application_1) + + SLEEP_SECONDS = 1 + time.sleep(SLEEP_SECONDS) + #group_variation = opendnp3.GroupVariationID(32, 2) + # time.sleep(SLEEP_SECONDS) + # print('\nReading status 1') + #application_1.master.ScanRange(group_variation, 0, 12) + # time.sleep(SLEEP_SECONDS) + # print('\nReading status 2') + #application_1.master.ScanRange(opendnp3.GroupVariationID(32, 2), 0, 3, opendnp3.TaskConfig().Default()) + #time.sleep(SLEEP_SECONDS) + print('\nReading status 3') + # application_1.slow_scan.Demand() + + # application_1.fast_scan_all.Demand() + + # for master in masters: + # master.fast_scan_all.Demand() + master = masters[0] + + # master.send_direct_operate_command_set(opendnp3.CommandSet( + # [ + # opendnp3.WithIndex(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), 0), + # opendnp3.WithIndex(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), 1) + # ]), + # command_callback) + + test_cmd = True + if test_cmd: + open_cmd = False + if open_cmd: + # Open + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 0, # PULSE/LATCH_ON to index 0 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 1, # PULSE/LATCH_ON to index 1 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 2, # PULSE/LATCH_ON to index 2 for open + command_callback) + else: + # Will need 5 minutes after open operation for this capbank + #Close + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 0, # PULSE/LATCH_ON to index 0 for close + # command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 0, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 1, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 2, # PULSE/LATCH_ON to index 0 for close + command_callback) + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), + # 1, + # command_callback) + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), + # 2, + # command_callback) + + + + while True: + cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} + for master in masters: + cim_msg = master.soe_handler.get_msg() + # print(cim_msg) + cim_full_msg['messages'].update(cim_msg) + + print(cim_full_msg) + time.sleep(5) + # master.send_select_and_operate_command() + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 1, + # command_callback) + + + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 1, + command_callback) + + print('\nStopping') + for master in masters: + master.shutdown() + # application_1.shutdown() + exit() + + # When terminating, it is necessary to set these to None so that + # it releases the shared pointer. Otherwise, python will not + # terminate (and even worse, the normal Ctrl+C won't help). + application_1.master.Disable() + application_1 = None + application_1.channel.Shutdown() + application_1.channel = None + application_1.manager.Shutdown() + +if __name__ == "__main__": + + import argparse + import json + parser = argparse.ArgumentParser() + parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) + # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') + args = parser.parse_args() + print(args.names) + # exit(0) + with open("device_ip_port_config_all.json") as f: + device_ip_port_config_all = json.load(f) + + device_ip_port_dict = device_ip_port_config_all[args.names[0]] + print(device_ip_port_dict) + run_master(device_ip_port_config_all, args.names) + # run_master(device_ip_port_dict['ip'], + # device_ip_port_dict['port'], + # device_ip_port_dict['link_local_addr'], + # device_ip_port_dict['conversion_type'], + # '632633') \ No newline at end of file From 834eb53d54756b1c3a7777d7f6852ff28325dd32 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 2 Mar 2021 14:54:03 -0700 Subject: [PATCH 42/64] Added master_cim_to_dnp3.py --- test/master_cim_to_dnp3.py | 193 +++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 test/master_cim_to_dnp3.py diff --git a/test/master_cim_to_dnp3.py b/test/master_cim_to_dnp3.py new file mode 100644 index 0000000..9cf5c71 --- /dev/null +++ b/test/master_cim_to_dnp3.py @@ -0,0 +1,193 @@ +import os +import sys +import time +sys.path.append("../dnp3/service") + +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.dnp3_to_cim import CIMMapping +from pydnp3 import opendnp3, openpal +import yaml +from dnp3.points import PointValue +# from dnp3.points import ( +# PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT +# ) + +master = None + +def collection_callback(result=None): + """ + :type result: opendnp3.CommandPointResult + """ + print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( + result.headerIndex, + result.index, + opendnp3.CommandPointStateToString(result.state), + opendnp3.CommandStatusToString(result.status) + )) + # print(result) + + +def command_callback(result=None): + """ + :type result: opendnp3.ICommandTaskResult + """ + print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) + result.ForeachItem(collection_callback) + +from gridappsd import GridAPPSD, DifferenceBuilder, utils +from time import sleep + +def on_message(simulation_id,message): + print("Message received:", simulation_id['message-id']) + print(message) + json_msg = yaml.safe_load(str(message)) + # self.master.apply_update(opendnp3.Binary(0), 12) + + # self.master.send_direct_operate_command( + # opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 5, + # command_callback) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + + global master + cap_point = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=0, op_type=None) + cap_point.measurement_id = "x" + # master = masters[0] + + # print(json_msg) + # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} + if "input" in json_msg.keys(): + print("input Jeff") + control_values = json_msg["input"]["message"]["forward_differences"] + print(control_values) + + for command in control_values: + print("command", command) + # master = self.master_dict[command["object"]] + # print(master) + point = cap_point + if command.get("object") == point.measurement_id and point.value != command.get("value"): + test_cmd = True + if test_cmd: + open_cmd = False + if open_cmd: + # Open + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 0, # PULSE/LATCH_ON to index 0 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 1, # PULSE/LATCH_ON to index 1 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 2, # PULSE/LATCH_ON to index 2 for open + command_callback) + cap_point.value = 0 + else: + # Will need 5 minutes after open operation for this capbank + # Close + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 0, # PULSE/LATCH_ON to index 0 for close + # command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 0, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 1, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 2, # PULSE/LATCH_ON to index 0 for close + command_callback) + cap_point.value = 1 + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), + # 1, + # command_callback) + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), + # 2, + # command_callback) + + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 1, + # command_callback) + +# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): +def run_master(device_ip_port_config_all, names): + masters = [] + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") + for name in names: + device_ip_port_dict = device_ip_port_config_all[name] + HOST=device_ip_port_dict['ip'] + PORT=device_ip_port_dict['port'] + DNP3_ADDR= device_ip_port_dict['link_local_addr'] + LocalAddr= device_ip_port_dict['link_remote_addr'] + convertion_type=device_ip_port_dict['conversion_type'] + object_name=device_ip_port_dict['CIM object'] + + application_1 = MyMaster(HOST=HOST, # "127.0.0.1 + LOCAL="0.0.0.0", + PORT=int(PORT), + DNP3_ADDR=int(DNP3_ADDR), + LocalAddr=int(LocalAddr), + log_handler=MyLogger(), + listener=AppChannelListener(), + soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), + master_application=MasterApplication()) + masters.append(application_1) + + SLEEP_SECONDS = 1 + time.sleep(SLEEP_SECONDS) + + global master + master = masters[0] + + simulation_id = str(1234) + gapps = GridAPPSD(simulation_id, address=utils.get_gridappsd_address(), + username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + + gapps.subscribe('/topic/goss.gridappsd.fim.output.' + simulation_id, on_message) + while True: + sleep(0.01) + + print('\nStopping') + for master in masters: + master.shutdown() + # application_1.shutdown() + exit() + + # When terminating, it is necessary to set these to None so that + # it releases the shared pointer. Otherwise, python will not + # terminate (and even worse, the normal Ctrl+C won't help). + application_1.master.Disable() + application_1 = None + application_1.channel.Shutdown() + application_1.channel = None + application_1.manager.Shutdown() + +if __name__ == "__main__": + + import argparse + import json + parser = argparse.ArgumentParser() + parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) + # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') + args = parser.parse_args() + print(args.names) + # exit(0) + with open("device_ip_port_config_all.json") as f: + device_ip_port_config_all = json.load(f) + + device_ip_port_dict = device_ip_port_config_all[args.names[0]] + print(device_ip_port_dict) + run_master(device_ip_port_config_all, args.names) + # run_master(device_ip_port_dict['ip'], + # device_ip_port_dict['port'], + # device_ip_port_dict['link_local_addr'], + # device_ip_port_dict['conversion_type'], + # '632633') \ No newline at end of file From 400196695735cbb62be3bd36a0e2f036f92abcd6 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 2 Mar 2021 15:00:52 -0700 Subject: [PATCH 43/64] Added master_cim_to_dnp3.py --- test/master_cim_to_dnp3.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/master_cim_to_dnp3.py b/test/master_cim_to_dnp3.py index 9cf5c71..788eb08 100644 --- a/test/master_cim_to_dnp3.py +++ b/test/master_cim_to_dnp3.py @@ -72,6 +72,7 @@ def on_message(simulation_id,message): # master = self.master_dict[command["object"]] # print(master) point = cap_point + # Capbank if command.get("object") == point.measurement_id and point.value != command.get("value"): test_cmd = True if test_cmd: @@ -104,14 +105,19 @@ def on_message(simulation_id,message): 2, # PULSE/LATCH_ON to index 0 for close command_callback) cap_point.value = 1 - - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), - # 1, - # command_callback) - - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), - # 2, - # command_callback) + pv_point_tmp = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) + pv_point_tmp.measurement_id = "x" + pv_points = [pv_point_tmp] + # PV points + for point in pv_points: + pass + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), + # 1, + # command_callback) + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), + # 2, + # command_callback) # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), # 1, From 7400060d10f18b77d6b8572866250f6072041e6f Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 2 Mar 2021 15:03:51 -0700 Subject: [PATCH 44/64] Added master_cim_to_dnp3.py --- test/master_cim_to_dnp3.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/master_cim_to_dnp3.py b/test/master_cim_to_dnp3.py index 788eb08..4f619a5 100644 --- a/test/master_cim_to_dnp3.py +++ b/test/master_cim_to_dnp3.py @@ -105,9 +105,11 @@ def on_message(simulation_id,message): 2, # PULSE/LATCH_ON to index 0 for close command_callback) cap_point.value = 1 - pv_point_tmp = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) - pv_point_tmp.measurement_id = "x" - pv_points = [pv_point_tmp] + pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) + pv_point_tmp1.measurement_id = "x" + pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) + pv_point_tmp2.measurement_id = "x" + pv_points = [pv_point_tmp1,pv_point_tmp2] # PV points for point in pv_points: pass From 0de35641784a6886d1b07aa8a2c7e5e78440dce0 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 3 Mar 2021 14:00:42 -0700 Subject: [PATCH 45/64] Updated master.py to add SOEHandlerSimple --- dnp3/service/dnp3/master.py | 45 +++- test/master_cim_to_dnp3.py | 405 ++++++++++++++++++------------------ test/master_main.py | 15 +- 3 files changed, 259 insertions(+), 206 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 63e1b85..7fae64c 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -255,6 +255,46 @@ def on_message(self, message): json_msg = yaml.safe_load(str(message)) print(json_msg) +class SOEHandlerSimple(opendnp3.ISOEHandler): + """ + Override ISOEHandler in this manner to implement application-specific sequence-of-events behavior. + + This is an interface for SequenceOfEvents (SOE) callbacks from the Master stack to the application layer. + """ + + def __init__(self): + super(SOEHandlerSimple, self).__init__() + + def Process(self, info, values): + """ + Process measurement data. + + :param info: HeaderInfo + :param values: A collection of values received from the Outstation (various data types are possible). + """ + visitor_class_types = { + opendnp3.ICollectionIndexedBinary: VisitorIndexedBinary, + opendnp3.ICollectionIndexedDoubleBitBinary: VisitorIndexedDoubleBitBinary, + opendnp3.ICollectionIndexedCounter: VisitorIndexedCounter, + opendnp3.ICollectionIndexedFrozenCounter: VisitorIndexedFrozenCounter, + opendnp3.ICollectionIndexedAnalog: VisitorIndexedAnalog, + opendnp3.ICollectionIndexedBinaryOutputStatus: VisitorIndexedBinaryOutputStatus, + opendnp3.ICollectionIndexedAnalogOutputStatus: VisitorIndexedAnalogOutputStatus, + opendnp3.ICollectionIndexedTimeAndInterval: VisitorIndexedTimeAndInterval + } + visitor_class = visitor_class_types[type(values)] + visitor = visitor_class() + values.Foreach(visitor) + for index, value in visitor.index_and_value: + log_string = 'SOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + + def Start(self): + _log.debug('In SOEHandler.Start') + + def End(self): + _log.debug('In SOEHandler.End') + class SOEHandler(opendnp3.ISOEHandler): """ @@ -442,7 +482,10 @@ def Process(self, info, values): # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) # self._cim_msg = cim_msg - self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) + temp_cim_msg ={'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} + + temp_cim_msg['message']['measurements'].update(self._cim_msg) + self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(temp_cim_msg)) # self._cim_msg = {"test":time.time()} print(self._cim_msg) diff --git a/test/master_cim_to_dnp3.py b/test/master_cim_to_dnp3.py index 4f619a5..05445b2 100644 --- a/test/master_cim_to_dnp3.py +++ b/test/master_cim_to_dnp3.py @@ -1,201 +1,206 @@ -import os -import sys -import time -sys.path.append("../dnp3/service") - -from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication -from dnp3.dnp3_to_cim import CIMMapping -from pydnp3 import opendnp3, openpal -import yaml -from dnp3.points import PointValue -# from dnp3.points import ( -# PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT -# ) - -master = None - -def collection_callback(result=None): - """ - :type result: opendnp3.CommandPointResult - """ - print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( - result.headerIndex, - result.index, - opendnp3.CommandPointStateToString(result.state), - opendnp3.CommandStatusToString(result.status) - )) - # print(result) - - -def command_callback(result=None): - """ - :type result: opendnp3.ICommandTaskResult - """ - print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) - result.ForeachItem(collection_callback) - -from gridappsd import GridAPPSD, DifferenceBuilder, utils -from time import sleep - -def on_message(simulation_id,message): - print("Message received:", simulation_id['message-id']) - print(message) - json_msg = yaml.safe_load(str(message)) - # self.master.apply_update(opendnp3.Binary(0), 12) - - # self.master.send_direct_operate_command( - # opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 5, - # command_callback) - - if type(json_msg) != dict: - raise ValueError( - ' is not a json formatted string.' - + '\njson_msg = {0}'.format(json_msg)) - - # fncs_input_message = {"{}".format(simulation_id): {}} - - global master - cap_point = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=0, op_type=None) - cap_point.measurement_id = "x" - # master = masters[0] - - # print(json_msg) - # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} - if "input" in json_msg.keys(): - print("input Jeff") - control_values = json_msg["input"]["message"]["forward_differences"] - print(control_values) - - for command in control_values: - print("command", command) - # master = self.master_dict[command["object"]] - # print(master) - point = cap_point - # Capbank - if command.get("object") == point.measurement_id and point.value != command.get("value"): - test_cmd = True - if test_cmd: - open_cmd = False - if open_cmd: - # Open - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 0, # PULSE/LATCH_ON to index 0 for open - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 1, # PULSE/LATCH_ON to index 1 for open - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 2, # PULSE/LATCH_ON to index 2 for open - command_callback) - cap_point.value = 0 - else: - # Will need 5 minutes after open operation for this capbank - # Close - # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 0, # PULSE/LATCH_ON to index 0 for close - # command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 0, # PULSE/LATCH_ON to index 0 for close - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 1, # PULSE/LATCH_ON to index 0 for close - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 2, # PULSE/LATCH_ON to index 0 for close - command_callback) - cap_point.value = 1 - pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) - pv_point_tmp1.measurement_id = "x" - pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) - pv_point_tmp2.measurement_id = "x" - pv_points = [pv_point_tmp1,pv_point_tmp2] - # PV points - for point in pv_points: - pass - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), - # 1, - # command_callback) - - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), - # 2, - # command_callback) - - # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 1, - # command_callback) - -# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): -def run_master(device_ip_port_config_all, names): - masters = [] - dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") - for name in names: - device_ip_port_dict = device_ip_port_config_all[name] - HOST=device_ip_port_dict['ip'] - PORT=device_ip_port_dict['port'] - DNP3_ADDR= device_ip_port_dict['link_local_addr'] - LocalAddr= device_ip_port_dict['link_remote_addr'] - convertion_type=device_ip_port_dict['conversion_type'] - object_name=device_ip_port_dict['CIM object'] - - application_1 = MyMaster(HOST=HOST, # "127.0.0.1 - LOCAL="0.0.0.0", - PORT=int(PORT), - DNP3_ADDR=int(DNP3_ADDR), - LocalAddr=int(LocalAddr), - log_handler=MyLogger(), - listener=AppChannelListener(), - soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), - master_application=MasterApplication()) - masters.append(application_1) - - SLEEP_SECONDS = 1 - time.sleep(SLEEP_SECONDS) - - global master - master = masters[0] - - simulation_id = str(1234) - gapps = GridAPPSD(simulation_id, address=utils.get_gridappsd_address(), - username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) - - gapps.subscribe('/topic/goss.gridappsd.fim.output.' + simulation_id, on_message) - while True: - sleep(0.01) - - print('\nStopping') - for master in masters: - master.shutdown() - # application_1.shutdown() - exit() - - # When terminating, it is necessary to set these to None so that - # it releases the shared pointer. Otherwise, python will not - # terminate (and even worse, the normal Ctrl+C won't help). - application_1.master.Disable() - application_1 = None - application_1.channel.Shutdown() - application_1.channel = None - application_1.manager.Shutdown() - -if __name__ == "__main__": - - import argparse - import json - parser = argparse.ArgumentParser() - parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) - # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') - args = parser.parse_args() - print(args.names) - # exit(0) - with open("device_ip_port_config_all.json") as f: - device_ip_port_config_all = json.load(f) - - device_ip_port_dict = device_ip_port_config_all[args.names[0]] - print(device_ip_port_dict) - run_master(device_ip_port_config_all, args.names) - # run_master(device_ip_port_dict['ip'], - # device_ip_port_dict['port'], - # device_ip_port_dict['link_local_addr'], - # device_ip_port_dict['conversion_type'], +import os +import sys +import time +sys.path.append("../dnp3/service") + +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication, SOEHandlerSimple +from dnp3.dnp3_to_cim import CIMMapping +from pydnp3 import opendnp3, openpal +import yaml +from dnp3.points import PointValue +# from dnp3.points import ( +# PointArray, PointDefinitions, PointDefinition, DNP3Exception, POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT +# ) + +master = None + +def collection_callback(result=None): + """ + :type result: opendnp3.CommandPointResult + """ + print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( + result.headerIndex, + result.index, + opendnp3.CommandPointStateToString(result.state), + opendnp3.CommandStatusToString(result.status) + )) + # print(result) + + +def command_callback(result=None): + """ + :type result: opendnp3.ICommandTaskResult + """ + print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) + result.ForeachItem(collection_callback) + +from gridappsd import GridAPPSD, DifferenceBuilder, utils +from time import sleep + +def on_message(simulation_id,message): + print("Message received:", simulation_id['message-id']) + print(message) + json_msg = yaml.safe_load(str(message)) + # self.master.apply_update(opendnp3.Binary(0), 12) + + # self.master.send_direct_operate_command( + # opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 5, + # command_callback) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + + global master + cap_point = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + # master = masters[0] + + # {"command": "update", "input": {"simulation_id": "1312790133", "message": {"timestamp": 1614730782, "difference_mrid": "e8287b64-6802-49b5-a6f0-12d8048fd98e", "reverse_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}], "forward_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}]}}} + + # print(json_msg) + # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} + if "input" in json_msg.keys(): + print("input Jeff") + control_values = json_msg["input"]["message"]["forward_differences"] + print(control_values) + + for command in control_values: + print("command", command) + # master = self.master_dict[command["object"]] + # print(master) + point = cap_point + # Capbank + if command.get("object") == point.measurement_id and point.value != command.get("value"): + open_cmd = command.get("value") == 0 + if open_cmd: + # Open + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 0, # PULSE/LATCH_ON to index 0 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 1, # PULSE/LATCH_ON to index 1 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 2, # PULSE/LATCH_ON to index 2 for open + command_callback) + cap_point.value = 0 + else: + # Will need 5 minutes after open operation for this capbank + # Close + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 0, # PULSE/LATCH_ON to index 0 for close + # command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 0, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 1, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 2, # PULSE/LATCH_ON to index 0 for close + command_callback) + cap_point.value = 1 + elif command.get("object") == point.measurement_id and point.value == command.get("value"): + print("Cap check", command.get("object"), command.get("value")) + + pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) + pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) + pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_points = [pv_point_tmp1,pv_point_tmp2] + # PV points + for point in pv_points: + pass + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), + # 1, + # command_callback) + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), + # 2, + # command_callback) + + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 1, + # command_callback) + +# def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): +def run_master(device_ip_port_config_all, names): + masters = [] + dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") + for name in names: + device_ip_port_dict = device_ip_port_config_all[name] + HOST=device_ip_port_dict['ip'] + PORT=device_ip_port_dict['port'] + DNP3_ADDR= device_ip_port_dict['link_local_addr'] + LocalAddr= device_ip_port_dict['link_remote_addr'] + convertion_type=device_ip_port_dict['conversion_type'] + object_name=device_ip_port_dict['CIM object'] + + application_1 = MyMaster(HOST=HOST, # "127.0.0.1 + LOCAL="0.0.0.0", + PORT=int(PORT), + DNP3_ADDR=int(DNP3_ADDR), + LocalAddr=int(LocalAddr), + log_handler=MyLogger(), + listener=AppChannelListener(), + # soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), + soe_handler=SOEHandlerSimple(), + master_application=MasterApplication()) + masters.append(application_1) + + SLEEP_SECONDS = 1 + time.sleep(SLEEP_SECONDS) + + global master + master = masters[0] + + simulation_id = str(1234) + gapps = GridAPPSD(simulation_id, address=utils.get_gridappsd_address(), + username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + + gapps.subscribe('/topic/goss.gridappsd.fim.output.' + simulation_id, on_message) + while True: + sleep(0.01) + + print('\nStopping') + for master in masters: + master.shutdown() + # application_1.shutdown() + exit() + + # When terminating, it is necessary to set these to None so that + # it releases the shared pointer. Otherwise, python will not + # terminate (and even worse, the normal Ctrl+C won't help). + application_1.master.Disable() + application_1 = None + application_1.channel.Shutdown() + application_1.channel = None + application_1.manager.Shutdown() + +if __name__ == "__main__": + + import argparse + import json + parser = argparse.ArgumentParser() + parser.add_argument("names", nargs='+', help="name of dnp3 outstation", type=str) + # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') + args = parser.parse_args() + print(args.names) + # exit(0) + with open("device_ip_port_config_all.json") as f: + device_ip_port_config_all = json.load(f) + + device_ip_port_dict = device_ip_port_config_all[args.names[0]] + print(device_ip_port_dict) + run_master(device_ip_port_config_all, args.names) + # run_master(device_ip_port_dict['ip'], + # device_ip_port_dict['port'], + # device_ip_port_dict['link_local_addr'], + # device_ip_port_dict['conversion_type'], # '632633') \ No newline at end of file diff --git a/test/master_main.py b/test/master_main.py index 7faf2d6..a43ae35 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -23,7 +23,10 @@ def build_csv_writers(folder, filename, column_names): # def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): def run_master(device_ip_port_config_all, names): masters = [] - dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") + data_loc = '.' + data_loc = '/media/sf_git_scada_read/GridAppsD_Usecase3/701_disag/701/test' +# dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") + dnp3_to_cim = CIMMapping(conversion_dict=os.path.join(data_loc,"conversion_dict_master.json"), model_line_dict=os.path.join(data_loc,"model_line_dict_master.json")) for name in names: device_ip_port_dict = device_ip_port_config_all[name] HOST=device_ip_port_dict['ip'] @@ -64,9 +67,9 @@ def run_master(device_ip_port_config_all, names): # for master in masters: # master.fast_scan_all.Demand() msg_count=0 - rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', 'rtu_7.csv', list(range(300))) + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', 'rtu1.csv', ['count']+list(range(300))) while True: - cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} + cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} for master in masters: cim_msg = master.soe_handler.get_msg() dnp3_msg = master.soe_handler.get_dnp3_msg() @@ -75,11 +78,13 @@ def run_master(device_ip_port_config_all, names): rtu_7_writer.writerow(np.insert(values,0, msg_count)) rtu_7_csvfile.flush() # print(cim_msg) - cim_full_msg['messages'].update(cim_msg) + # message['message']['measurements'] + cim_full_msg['message']['measurements'].update(cim_msg) + # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(temp_cim_msg)) msg_count+=1 print(cim_full_msg) - time.sleep(30) + time.sleep(10) print('\nStopping') From 713a601972c98dec733feb34a33e10b6e18a8772 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 3 Mar 2021 14:04:02 -0700 Subject: [PATCH 46/64] Updated to move publish and csv files. --- dnp3/service/dnp3/master.py | 8 +++--- test/master_cim_to_dnp3.py | 5 +++- test/master_main.py | 51 ++++++++++++++++++++++++------------- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 63e1b85..de14626 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -267,9 +267,9 @@ def __init__(self, name, device, dnp3_to_cim): self._name = name self._device = device self._dnp3_to_cim = dnp3_to_cim - self._gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), - username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) - self._gapps.subscribe('/topic/goss.gridappsd.fim.input.'+str(1234), on_message) + # self._gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), + # username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + # self._gapps.subscribe('/topic/goss.gridappsd.fim.input.'+str(1234), on_message) self._cim_msg = {} self._dnp3_msg = {} @@ -442,7 +442,7 @@ def Process(self, info, values): # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) # self._cim_msg = cim_msg - self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) + # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) # self._cim_msg = {"test":time.time()} print(self._cim_msg) diff --git a/test/master_cim_to_dnp3.py b/test/master_cim_to_dnp3.py index 4f619a5..b7a1780 100644 --- a/test/master_cim_to_dnp3.py +++ b/test/master_cim_to_dnp3.py @@ -57,8 +57,11 @@ def on_message(simulation_id,message): global master cap_point = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=0, op_type=None) - cap_point.measurement_id = "x" + cap_point.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + # master = masters[0] + # {"command": "update", "input": {"simulation_id": "1312790133", "message": {"timestamp": 1614730782, "difference_mrid": "e8287b64-6802-49b5-a6f0-12d8048fd98e", "reverse_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}], "forward_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}]}}} # print(json_msg) # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} diff --git a/test/master_main.py b/test/master_main.py index 7faf2d6..68ce469 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -9,6 +9,9 @@ from dnp3.dnp3_to_cim import CIMMapping from pydnp3 import opendnp3, openpal +from gridappsd.topics import simulation_output_topic, simulation_input_topic +from gridappsd import GridAPPSD, DifferenceBuilder, utils + def build_csv_writers(folder, filename, column_names): _file = os.path.join(folder, filename) @@ -22,6 +25,8 @@ def build_csv_writers(folder, filename, column_names): # def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): def run_master(device_ip_port_config_all, names): + gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), + username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) masters = [] dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") for name in names: @@ -42,6 +47,7 @@ def run_master(device_ip_port_config_all, names): listener=AppChannelListener(), soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), master_application=MasterApplication()) + application_1.name=name # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) masters.append(application_1) @@ -64,23 +70,30 @@ def run_master(device_ip_port_config_all, names): # for master in masters: # master.fast_scan_all.Demand() msg_count=0 - rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', 'rtu_7.csv', list(range(300))) + csv_dict = {} + for master in masters: + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'.csv', list(range(300))) + csv_dict[master.name] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} + cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{}} while True: - cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} for master in masters: cim_msg = master.soe_handler.get_msg() dnp3_msg = master.soe_handler.get_dnp3_msg() - max_index = max(dnp3_msg.keys()) - values = [dnp3_msg[k] for k in range(max_index)] - rtu_7_writer.writerow(np.insert(values,0, msg_count)) - rtu_7_csvfile.flush() + if len(dnp3_msg.keys()) > 0: + max_index = max(dnp3_msg.keys()) + values = [dnp3_msg[k] for k in range(max_index)] + csv_dict[master.name]['csv_writer'].writerow(np.insert(values,0, msg_count)) + csv_dict[master.name]['csv_file'].flush() + else: + print("no data yet") + # print(cim_msg) - cim_full_msg['messages'].update(cim_msg) + cim_full_msg['message'].update(cim_msg) + gapps.send('/topic/goss.gridappsd.fim.input.' + str(1234), json.dumps(cim_msg)) msg_count+=1 - print(cim_full_msg) - time.sleep(30) - + print(cim_full_msg) + time.sleep(30) print('\nStopping') for master in masters: @@ -106,15 +119,17 @@ def run_master(device_ip_port_config_all, names): # parser.add_argument("feeder_info",help='feeder info directory for y-matrix, etc.') args = parser.parse_args() print(args.names) - # exit(0) + names = args.names + with open("device_ip_port_config_all.json") as f: device_ip_port_config_all = json.load(f) + if args.names[0] == '*': + names = [] + device_names = device_ip_port_config_all.keys() + for device_name in device_names: + if 'test' not in device_names: + names.append(device_name) - device_ip_port_dict = device_ip_port_config_all[args.names[0]] + device_ip_port_dict = device_ip_port_config_all[names[0]] print(device_ip_port_dict) - run_master(device_ip_port_config_all, args.names) - # run_master(device_ip_port_dict['ip'], - # device_ip_port_dict['port'], - # device_ip_port_dict['link_local_addr'], - # device_ip_port_dict['conversion_type'], - # '632633') \ No newline at end of file + run_master(device_ip_port_config_all, names) From 8d3e22581bedd68123f753ba239de7c231b054ef Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 5 Mar 2021 11:56:29 -0700 Subject: [PATCH 47/64] Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- test/master_main.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/master_main.py b/test/master_main.py index 78b6f11..8b8c7cc 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -4,8 +4,12 @@ import csv import platform import numpy as np +import yaml + sys.path.append("../dnp3/service") +from CIMProcessor import CIMProcessor + from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication from dnp3.dnp3_to_cim import CIMMapping from pydnp3 import opendnp3, openpal @@ -13,6 +17,7 @@ from gridappsd.topics import simulation_output_topic, simulation_input_topic from gridappsd import GridAPPSD, DifferenceBuilder, utils +myCIMProcessor = None def build_csv_writers(folder, filename, column_names): _file = os.path.join(folder, filename) @@ -24,6 +29,12 @@ def build_csv_writers(folder, filename, column_names): # csv_writer.writerow(['epoch time'] + column_names) return file_handle, csv_writer +def on_message(message): + json_msg = yaml.safe_load(str(message)) + print(json_msg) + # find point to master list + myCIMProcessor.process(message) + # def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): def run_master(device_ip_port_config_all, names): gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), @@ -56,6 +67,10 @@ def run_master(device_ip_port_config_all, names): # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) masters.append(application_1) + if name == 'RTU1': + global myCIMProcessor + point_definitions = None + myCIMProcessor = CIMProcessor(point_definitions,application_1) SLEEP_SECONDS = 1 time.sleep(SLEEP_SECONDS) @@ -96,7 +111,7 @@ def run_master(device_ip_port_config_all, names): # print(cim_msg) # message['message']['measurements'] cim_full_msg['message']['measurements'].update(cim_msg) - # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(temp_cim_msg)) + gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_full_msg)) msg_count+=1 print(cim_full_msg) From 8afb38d08aaf43e27930fd46afead2f96bf5ca8d Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 5 Mar 2021 11:56:58 -0700 Subject: [PATCH 48/64] Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- test/CIMProcessor.py | 124 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 test/CIMProcessor.py diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py new file mode 100644 index 0000000..97a479a --- /dev/null +++ b/test/CIMProcessor.py @@ -0,0 +1,124 @@ + +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication, SOEHandlerSimple +from dnp3.dnp3_to_cim import CIMMapping +from pydnp3 import opendnp3, openpal +import yaml +from dnp3.points import PointValue + +def collection_callback(result=None): + """ + :type result: opendnp3.CommandPointResult + """ + print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( + result.headerIndex, + result.index, + opendnp3.CommandPointStateToString(result.state), + opendnp3.CommandStatusToString(result.status) + )) + # print(result) + + +def command_callback(result=None): + """ + :type result: opendnp3.ICommandTaskResult + """ + print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) + result.ForeachItem(collection_callback) + +class CIMProcessor(object): + + def __init__(self, point_definitions,master): + self.point_definitions = point_definitions # TODO + self._master = master + + def process(self, message): + master = self._master + # print("Message received:", simulation_id['message-id']) + print(message) + json_msg = yaml.safe_load(str(message)) + # self.master.apply_update(opendnp3.Binary(0), 12) + + # self.master.send_direct_operate_command( + # opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 5, + # command_callback) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} + + cap_point = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + # master = masters[0] + + # {"command": "update", "input": {"simulation_id": "1312790133", "message": {"timestamp": 1614730782, "difference_mrid": "e8287b64-6802-49b5-a6f0-12d8048fd98e", "reverse_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}], "forward_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}]}}} + + # print(json_msg) + # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} + if "input" in json_msg.keys(): + print("input Jeff") + control_values = json_msg["input"]["message"]["forward_differences"] + print(control_values) + + for command in control_values: + print("command", command) + # master = self.master_dict[command["object"]] + # print(master) + point = cap_point + # Capbank + if command.get("object") == point.measurement_id and point.value != command.get("value"): + open_cmd = command.get("value") == 0 + if open_cmd: + # Open + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 0, # PULSE/LATCH_ON to index 0 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 1, # PULSE/LATCH_ON to index 1 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 2, # PULSE/LATCH_ON to index 2 for open + command_callback) + cap_point.value = 0 + else: + # Will need 5 minutes after open operation for this capbank + # Close + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 0, # PULSE/LATCH_ON to index 0 for close + # command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 0, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 1, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 2, # PULSE/LATCH_ON to index 0 for close + command_callback) + cap_point.value = 1 + elif command.get("object") == point.measurement_id and point.value == command.get("value"): + print("Cap check", command.get("object"), command.get("value")) + + pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) + pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) + pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_points = [pv_point_tmp1,pv_point_tmp2] + # PV points + for point in pv_points: + pass + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), + # 1, + # command_callback) + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), + # 2, + # command_callback) + + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 1, + # command_callback) \ No newline at end of file From 51fd6598f86ae54f013d6b510a4f77a24052b5a6 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 5 Mar 2021 12:20:11 -0700 Subject: [PATCH 49/64] Updated to subscribe --- test/master_main.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/master_main.py b/test/master_main.py index 8b8c7cc..e81444f 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -5,6 +5,7 @@ import platform import numpy as np import yaml +import logging sys.path.append("../dnp3/service") @@ -19,6 +20,8 @@ myCIMProcessor = None +_log = logging.getLogger(__name__) + def build_csv_writers(folder, filename, column_names): _file = os.path.join(folder, filename) if os.path.exists(_file): @@ -29,9 +32,12 @@ def build_csv_writers(folder, filename, column_names): # csv_writer.writerow(['epoch time'] + column_names) return file_handle, csv_writer -def on_message(message): +def on_message(simulation_id, message): json_msg = yaml.safe_load(str(message)) + print('Receive message') print(json_msg) + # _log.info("Receive message") + # _log.info(json_msg) # find point to master list myCIMProcessor.process(message) @@ -39,6 +45,7 @@ def on_message(message): def run_master(device_ip_port_config_all, names): gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) + gapps.subscribe('/topic/goss.gridappsd.fim.output.' + str('1234'), on_message) masters = [] data_loc = '.' if 'Darwin' != platform.system(): @@ -114,7 +121,8 @@ def run_master(device_ip_port_config_all, names): gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_full_msg)) msg_count+=1 - print(cim_full_msg) + print(str(cim_full_msg)[:20]) + _log.info(cim_full_msg) time.sleep(30) From d24b352a7103401dcc4b913d00ef3c9529dffe9d Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 9 Mar 2021 11:36:21 -0700 Subject: [PATCH 50/64] Dialed back the print statements. Fixed to work with all RTUs --- dnp3/service/dnp3/master.py | 4 +- test/CIMProcessor.py | 90 ++++++++++++++++++++++--------------- test/master_main.py | 14 +++--- 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 8c284f8..92b038f 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -423,7 +423,7 @@ def Process(self, info, values): element_attr_to_mrid = model_line_dict[self._name] model = model_line_dict[self._name] conversion = conversion_dict[self._device] - print(conversion) + # print(conversion) ## TODO check for each type seperate binary vs analog ## Analog check @@ -485,7 +485,7 @@ def Process(self, info, values): # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) # self._cim_msg = {"test":time.time()} - print(self._cim_msg) + print(str(self._cim_msg)[:80]) def Start(self): _log.debug('In SOEHandler.Start') diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index 97a479a..1783f0c 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -49,9 +49,24 @@ def process(self, message): + '\njson_msg = {0}'.format(json_msg)) # fncs_input_message = {"{}".format(simulation_id): {}} - - cap_point = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) - cap_point.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" +# "capacitors":[ +# {"name":"701-104cf","mRID":"_5955BE75-5EE5-477A-936F-65EDE5E3B831","CN1":"cf_701_311","phases":"ABC","kvar_A":400.0,"kvar_B":400.0,"kvar_C":400.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_70799_mc","monitoredClass":"ACLineSegment","monitoredBus":"fu_701_9492","monitoredPhase":"B"}, +# {"name":"701-275cw","mRID":"_C1706031-2C1C-464C-8376-6A51FA70B470","CN1":"fu_701_20042","phases":"ABC","kvar_A":100.0,"kvar_B":100.0,"kvar_C":100.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_167489_mc","monitoredClass":"ACLineSegment","monitoredBus":"cw_701_1710","monitoredPhase":"B"}, +# {"name":"701-319cw","mRID":"_245E3924-8292-46D5-A11E-C80F7D6EE253","CN1":"cw_701_347","phases":"ABC","kvar_A":400.0,"kvar_B":400.0,"kvar_C":400.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_57838_mc","monitoredClass":"ACLineSegment","monitoredBus":"fu_701_9026","monitoredPhase":"B"} +# ], +# "regulators":[ +# {"bankName":"s1","size":"1","bankPhases":"ABC","tankName":[""],"endNumber":[2],"endPhase":["ABC"],"rtcName":["s1"],"mRID":["_D081C22C-D840-4303-8C95-C9151610C9A6"],"monitoredPhase":["A"],"TapChanger.tculControlMode":["volt"],"highStep":[16],"lowStep":[-16],"neutralStep":[0],"normalStep":[0],"TapChanger.controlEnabled":[true],"lineDropCompensation":[true],"ltcFlag":[true],"RegulatingControl.enabled":[true],"RegulatingControl.discrete":[true],"RegulatingControl.mode":["voltage"],"step":[-8],"targetValue":[123.0000],"targetDeadband":[2.0000],"limitVoltage":[0.0000],"stepVoltageIncrement":[0.6250],"neutralU":[6927.6000],"initialDelay":[30.0000],"subsequentDelay":[2.0000],"lineDropR":[7.0000],"lineDropX":[0.0000],"reverseLineDropR":[0.0000],"reverseLineDropX":[0.0000],"ctRating":[300.0000],"ctRatio":[1500.0000],"ptRatio":[57.7300]} +# ], + + cap_point1 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point1.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + cap_point2 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point2.measurement_id = "_C1706031-2C1C-464C-8376-6A51FA70B470" + cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point3.measurement_id = "_245E3924-8292-46D5-A11E-C80F7D6EE253" + cap_list = [cap_point1,cap_point2,cap_point3] + reg_point1.measurement_id = "_D081C22C-D840-4303-8C95-C9151610C9A6" + ref_list = [reg_point1] # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" # master = masters[0] @@ -68,40 +83,41 @@ def process(self, message): print("command", command) # master = self.master_dict[command["object"]] # print(master) - point = cap_point - # Capbank - if command.get("object") == point.measurement_id and point.value != command.get("value"): - open_cmd = command.get("value") == 0 - if open_cmd: - # Open - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 0, # PULSE/LATCH_ON to index 0 for open - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 1, # PULSE/LATCH_ON to index 1 for open - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 2, # PULSE/LATCH_ON to index 2 for open - command_callback) - cap_point.value = 0 - else: - # Will need 5 minutes after open operation for this capbank - # Close - # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 0, # PULSE/LATCH_ON to index 0 for close - # command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 0, # PULSE/LATCH_ON to index 0 for close - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 1, # PULSE/LATCH_ON to index 0 for close - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 2, # PULSE/LATCH_ON to index 0 for close - command_callback) - cap_point.value = 1 - elif command.get("object") == point.measurement_id and point.value == command.get("value"): - print("Cap check", command.get("object"), command.get("value")) + point = cap_point1 + for point in cap_list: + # Capbank + if command.get("object") == point.measurement_id and point.value != command.get("value"): + open_cmd = command.get("value") == 0 + if open_cmd: + # Open + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 0, # PULSE/LATCH_ON to index 0 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 1, # PULSE/LATCH_ON to index 1 for open + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + 2, # PULSE/LATCH_ON to index 2 for open + command_callback) + cap_point.value = 0 + else: + # Will need 5 minutes after open operation for this capbank + # Close + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 0, # PULSE/LATCH_ON to index 0 for close + # command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 0, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 1, # PULSE/LATCH_ON to index 0 for close + command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + 2, # PULSE/LATCH_ON to index 0 for close + command_callback) + cap_point.value = 1 + elif command.get("object") == point.measurement_id and point.value == command.get("value"): + print("Cap check", command.get("object"), command.get("value")) pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" diff --git a/test/master_main.py b/test/master_main.py index e81444f..3e2049c 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -101,10 +101,11 @@ def run_master(device_ip_port_config_all, names): for master in masters: rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'.csv', list(range(300))) csv_dict[master.name] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} - cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{}} + cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} while True: - cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} + # cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} for master in masters: + print("getting CIM from master "+master.name) cim_msg = master.soe_handler.get_msg() dnp3_msg = master.soe_handler.get_dnp3_msg() if len(dnp3_msg.keys()) > 0: @@ -121,9 +122,9 @@ def run_master(device_ip_port_config_all, names): gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_full_msg)) msg_count+=1 - print(str(cim_full_msg)[:20]) + print(master.name+" " +str(cim_full_msg)[:100]) _log.info(cim_full_msg) - time.sleep(30) + time.sleep(5) print('\nStopping') @@ -158,9 +159,12 @@ def run_master(device_ip_port_config_all, names): names = [] device_names = device_ip_port_config_all.keys() for device_name in device_names: - if 'test' not in device_names: + if 'RTU' in device_name: names.append(device_name) + print("Running "+ str(names)) + time.sleep(2) + device_ip_port_dict = device_ip_port_config_all[names[0]] print(device_ip_port_dict) run_master(device_ip_port_config_all, names) From b2f2c73ea891dd229ed13e6cb67a8f4ce1eb187f Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 9 Mar 2021 17:25:13 -0700 Subject: [PATCH 51/64] Updated for LTU and moved message sub to after Master creation --- test/CIMProcessor.py | 17 +++++++++++++---- test/master_main.py | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index 1783f0c..9e02318 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -65,8 +65,9 @@ def process(self, message): cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) cap_point3.measurement_id = "_245E3924-8292-46D5-A11E-C80F7D6EE253" cap_list = [cap_point1,cap_point2,cap_point3] + reg_point1 = PointValue(command_type=None, function_code=None, value=100, point_def=0, index=0, op_type=None) reg_point1.measurement_id = "_D081C22C-D840-4303-8C95-C9151610C9A6" - ref_list = [reg_point1] + reg_list = [reg_point1] # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" # master = masters[0] @@ -83,7 +84,15 @@ def process(self, message): print("command", command) # master = self.master_dict[command["object"]] # print(master) - point = cap_point1 + for point in reg_list: + if command.get("object") == point.measurement_id and point.value != command.get("value"): + point.value = command.get("value") + print("Send reg value "+ str(command.get("value"))) + temp_value = int(command.get("value")) + master.send_direct_operate_command(opendnp3.AnalogOutputInt32(temp_value), + 0, + command_callback) + for point in cap_list: # Capbank if command.get("object") == point.measurement_id and point.value != command.get("value"): @@ -99,7 +108,7 @@ def process(self, message): master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), 2, # PULSE/LATCH_ON to index 2 for open command_callback) - cap_point.value = 0 + point.value = 0 else: # Will need 5 minutes after open operation for this capbank # Close @@ -115,7 +124,7 @@ def process(self, message): master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), 2, # PULSE/LATCH_ON to index 0 for close command_callback) - cap_point.value = 1 + point.value = 1 elif command.get("object") == point.measurement_id and point.value == command.get("value"): print("Cap check", command.get("object"), command.get("value")) diff --git a/test/master_main.py b/test/master_main.py index 3e2049c..2f0802d 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -45,7 +45,7 @@ def on_message(simulation_id, message): def run_master(device_ip_port_config_all, names): gapps = GridAPPSD(1234, address=utils.get_gridappsd_address(), username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) - gapps.subscribe('/topic/goss.gridappsd.fim.output.' + str('1234'), on_message) + masters = [] data_loc = '.' if 'Darwin' != platform.system(): @@ -78,6 +78,8 @@ def run_master(device_ip_port_config_all, names): global myCIMProcessor point_definitions = None myCIMProcessor = CIMProcessor(point_definitions,application_1) + + gapps.subscribe('/topic/goss.gridappsd.fim.output.' + str('1234'), on_message) SLEEP_SECONDS = 1 time.sleep(SLEEP_SECONDS) From bc7b9cd20808aad98845168dc40269be489ff59f Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 10 Mar 2021 14:05:07 -0700 Subject: [PATCH 52/64] Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- test/build_conversion_dicts.py | 262 +++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 test/build_conversion_dicts.py diff --git a/test/build_conversion_dicts.py b/test/build_conversion_dicts.py new file mode 100644 index 0000000..1769c08 --- /dev/null +++ b/test/build_conversion_dicts.py @@ -0,0 +1,262 @@ + +import pandas as pd +import json +import numpy as np + +def get_conversion_model(csv_file,sheet_name): + df = pd.read_excel(csv_file,sheet_name=sheet_name) + df = df.replace(np.nan, '', regex=True) + master_dict ={ + 'Analog input':{}, + 'Analog output':{}, + 'Binary input':{}, + 'Binary output':{} } + +# temp_df = pd.DataFrame(df[1:4]) +# temp_df = temp_df.set_index('Index') +# master_dict['Analog input'] = temp_df.T + +# temp_df = pd.DataFrame(df[6:10]) +# temp_df = temp_df.set_index('Index') +# master_dict['Analog output'] = temp_df.T + + x = [] + + for index, row in df.iterrows(): +# print(pd.isna(row['Multiplier'])) + if pd.isna(row['Multiplier']) or row['Multiplier'] == '': +# print(index) + x.append(index) + print(df.shape) + x.append(df.shape[0]) + it = iter(x) + for x in it: +# print(x) + type_name = df.iloc[x][1] + print(type_name) + next_value = next(it) + print (x+1, next_value) + print(pd.DataFrame(df[x+1:next_value])) + temp_df = pd.DataFrame(df[x+1: next_value]) +# temp_df = temp_df.set_index('Index') +# temp_df = temp_df.replace(np.nan, '', regex=True) + temp_dict = temp_df.T.to_dict() + temp_dict_set_key_to_index = {} + for k,v in temp_dict.items(): + temp_dict_set_key_to_index[int(v['Index'])] = v + master_dict[type_name] = temp_dict_set_key_to_index + return master_dict + + +def convert_rtu(csv_file=r'DNP3_Dict_Feb23(3).xlsx', sheet_name='RTU1'): + skiprows = 0 + if sheet_name == 'RTU1': + skiprows = 1 + df = pd.read_excel(csv_file, skiprows=skiprows, sheet_name=sheet_name) + df = df.replace(np.nan, '', regex=True) + master_dict = {} + conversion_name_dict = {} + phase_dict = {'1': 'A', '2': 'B', '3': 'C'} + for index, row in df.iterrows(): + rtu_string = sheet_name + if rtu_string not in master_dict: + master_dict[rtu_string] = {'Analog input': {}} + # conversion_name_dict[rtu_string] = [] + temp_dict = row.to_dict() + index = temp_dict['Index'] + temp_dict['Index'] = index + temp_name = temp_dict['Name'] + if not temp_name.strip(): + print('empty') + break + if 'LTC_' in temp_name: + processed_name = temp_name + '_A' # fake phase added I don't know what to do with this + else: + last_underscore_index = temp_name.rindex('_') + processed_name = temp_name[:last_underscore_index] + temp_type = processed_name[0] + + temp_phase = processed_name[-1] + processes_dict = {} + # break + processes_dict['orig_name'] = temp_name + processes_dict['index'] = index + processes_dict['Multiplier'] = 1 + processes_dict['CIM phase'] = temp_phase + processes_dict['CIM attribute'] = 'magnitude' + processes_dict['CIM units'] = 'VA' + processes_dict['CIM name'] = processed_name[2:-2].lower() # Hope this is good :) + # print(temp_dict) + # break + name_phase = processed_name + if name_phase in conversion_name_dict: + print('duplicate name', name_phase) + else: + conversion_name_dict[name_phase] = processes_dict + if temp_type == 'V': + processes_dict['CIM units'] = 'PNV' + + master_dict[rtu_string]['Analog input'][index] = processes_dict + return master_dict, conversion_name_dict + +def get_device_dict(model_dict, model_line_dict, device_type, name): + if device_type == 'Shark': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('ACLineSegment_'+name): + # print(meas) + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + elif device_type == 'RTU': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('EnergyConsumer_'+name): + # print(meas) + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + + elif device_type == 'Beckwith CapBank': + #LinearShuntCompensator + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('LinearShuntCompensator_'+name): + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + elif meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'Pos': + if meas['measurementType'] not in model_line_dict[name]: + model_line_dict[name][meas['measurementType']] = {} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'pos'} + for cap in model_dict['feeders'][0]["capacitors"]: + if cap['name'] == name: + model_line_dict[name]['manual close'] = {'mrid':meas['mRID'],'type':'magnitude'} + print(cap) + #TODO figure this out + # 0 manual close + # 1 manual open + elif device_type == 'Beckwith LTC': + for meas in model_dict['feeders'][0]['measurements']: + if meas['name'].startswith('RatioTapChanger_'+name):#ConductingEquipment_name + if meas['measurementType'] == 'PNV': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + elif meas['measurementType'] == 'VA': + if meas['measurementType'] not in model_line_dict[name]: model_line_dict[name][meas['measurementType']] ={} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'magnitude'} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'angle'} + if meas['measurementType'] == 'Pos': + if meas['measurementType'] not in model_line_dict[name]: + model_line_dict[name][meas['measurementType']] = {} + model_line_dict[name][meas['measurementType']][meas['phases']] = {'mrid':meas['mRID'],'type':'pos'} + for reg in model_dict['feeders'][0]["regulators"]: + if reg['bankName'] == name: + # Do I have to count for each position change? + print(reg) + +def build_RTAC(filename_rtu=r'DNP3_Dict_Feb23(3).xlsx', + filename_eq='DNP3 list.xlsx', + sd_g_model=''): + conversion_dict_master = {} + conversion_name_dict_master = {} + + conversion_dict, conversion_name_dict = convert_rtu(csv_file=filename_rtu, sheet_name='RTU1') + conversion_dict_master.update(conversion_dict) + conversion_name_dict_master.update(conversion_name_dict) + + conversion_dict, conversion_name_dict = convert_rtu(csv_file=filename_rtu, sheet_name='RTU2') + conversion_dict_master.update(conversion_dict) + conversion_name_dict_master.update(conversion_name_dict) + + conversion_dict, conversion_name_dict = convert_rtu(csv_file=filename_rtu, sheet_name='RTU3') + conversion_dict_master.update(conversion_dict) + conversion_name_dict_master.update(conversion_name_dict) + + conversion_dict, conversion_name_dict = convert_rtu(csv_file=filename_rtu, sheet_name='RTU4') + conversion_dict_master.update(conversion_dict) + conversion_name_dict_master.update(conversion_name_dict) + + conversion_dict, conversion_name_dict = convert_rtu(csv_file=filename_rtu, sheet_name='RTU5') + conversion_dict_master.update(conversion_dict) + conversion_name_dict_master.update(conversion_name_dict) + + conversion_dict, conversion_name_dict = convert_rtu(csv_file=filename_rtu, sheet_name='RTU6') + conversion_dict_master.update(conversion_dict) + conversion_name_dict_master.update(conversion_name_dict) + + conversion_dict, conversion_name_dict = convert_rtu(csv_file=filename_rtu, sheet_name='RTU7') + conversion_dict_master.update(conversion_dict) + conversion_name_dict_master.update(conversion_name_dict) + + ## individual equipment conversion parts, saves to file + build_eq_conversion_dict(filename_eq) + + # Load EQ model + with open('conversion_dict_eq.json') as json_file: + data = json.load(json_file) + + data.update(conversion_dict_master) + + with open("conversion_dict_master.json", "w") as f: + json.dump(data, f, indent=2) + conversion_dict = data + + with open(sd_g_model) as f: + model_dict = json.load(f) + + model_line_dict = {} + + device_type = "Beckwith CapBank" + name = "701-104cf" + model_line_dict[name] = {} + get_device_dict(model_dict, model_line_dict, device_type, name) + + device_type = 'Beckwith LTC' + name = "s1" + model_line_dict[name] = {} + get_device_dict(model_dict, model_line_dict, device_type, name) + + for full_name, name_dict in conversion_name_dict_master.items(): + name = name_dict['CIM name'] + device_type = "RTU" + # name='tran_xf_701_1095507_5022' + model_line_dict[name] = {} + get_device_dict(model_dict, model_line_dict, device_type, name) + + with open("model_line_dict_master.json", "w") as f: + json.dump(model_line_dict, f, indent=2) + + +def build_eq_conversion_dict(csv_file = 'DNP3 list.xlsx'): + conversion_dict_eq = {} + # csv_file = 'DNP3 list.xlsx' + shark = get_conversion_model(csv_file, sheet_name='Shark') + conversion_dict_eq['Shark'] = shark + sheet_name = 'Beckwith CapBank 2' + beckwith_capbank = get_conversion_model(csv_file, sheet_name) + conversion_dict_eq['Beckwith CapBank'] = beckwith_capbank + sheet_name = 'Beckwith LTC' + beckwith_capbank = get_conversion_model(csv_file, sheet_name) + conversion_dict_eq[sheet_name] = beckwith_capbank + with open("conversion_dict_eq.json", "w") as f: + json.dump(conversion_dict_eq, f, indent=2) + + +if __name__ == '__main__': + build_RTAC(filename_rtu=r'DNP3_Dict_Feb23(3).xlsx', + filename_eq='DNP3 list.xlsx', + sd_g_model = '/Users/jsimpson/git/adms-use-case-3/GridAppsD_Usecase3/701_disag/701/221053913/model_dict.json') \ No newline at end of file From 939960cf862a5829771e88aa76d1fc607078c88e Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 12 Mar 2021 12:26:23 -0700 Subject: [PATCH 53/64] Added pv points --- test/CIMProcessor.py | 32 ++++++++++++++++++++++---------- test/master_main.py | 27 +++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index 9e02318..1645473 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -30,6 +30,7 @@ class CIMProcessor(object): def __init__(self, point_definitions,master): self.point_definitions = point_definitions # TODO self._master = master + self._pv_points = point_definitions def process(self, message): master = self._master @@ -130,19 +131,30 @@ def process(self, message): pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_point_tmp1.attribute='PowerElectronicsConnection.p' +# command {'object': '_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B', 'attribute': 'PowerElectronicsConnection.p', 'value': 17.27693169381238} +# PV +# PV +# command {'object': '_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B', 'attribute': 'PowerElectronicsConnection.q', 'value': 41.028701716375124} + # pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" - pv_points = [pv_point_tmp1,pv_point_tmp2] + pv_point_tmp2.attribute='PowerElectronicsConnection.q' + pv_points=[pv_point_tmp1,pv_point_tmp2] # PV points - for point in pv_points: - pass - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(7), - # 1, - # command_callback) - - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(14), - # 2, - # command_callback) + for point in self._pv_points: + if command.get("object") == point.measurement_id and point.value != command.get("value"): + if command.get("attribute") == point.attribute: + temp_index = point.index + point.value = int(command.get("value")) + print("PV ",point.index, point.value, point.attribute) + master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), + temp_index, + command_callback) + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), + # 2, + # command_callback) # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), # 1, diff --git a/test/master_main.py b/test/master_main.py index 2f0802d..96560db 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -76,9 +76,32 @@ def run_master(device_ip_port_config_all, names): masters.append(application_1) if name == 'RTU1': global myCIMProcessor - point_definitions = None - myCIMProcessor = CIMProcessor(point_definitions,application_1) + conversion_dict=os.path.join(data_loc,"conversion_dict_master.json") + with open(conversion_dict) as f: + conversion_dict = json.load(f) + # print(conversion_dict['RTU1']['Analog output']) + from dnp3.points import PointValue + + + pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) + pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) + pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_points = [pv_point_tmp1,pv_point_tmp2] + pv_points =[] + for k,v in conversion_dict['RTU1']['Analog output'].items(): + if 'CIM mRID' in v: + pv_point = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=int(v['index']), op_type=None) + pv_point.measurement_id = v['CIM mRID'] + pv_point.attribute = v['CIM attribute'] + pv_points.append(pv_point) + + # just build points for PV + point_definitions = None + + myCIMProcessor = CIMProcessor(pv_points,application_1) + gapps.subscribe('/topic/goss.gridappsd.fim.output.' + str('1234'), on_message) SLEEP_SECONDS = 1 From 0434f8b48b36e15d41c2b85715c389d8a3e909cd Mon Sep 17 00:00:00 2001 From: jsimpson Date: Mon, 15 Mar 2021 17:07:27 -0600 Subject: [PATCH 54/64] Fixed csv header, added shark meter --- test/CIMProcessor.py | 7 +++++-- test/master_main.py | 33 ++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index 1645473..53b59d9 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -146,9 +146,12 @@ def process(self, message): if command.get("object") == point.measurement_id and point.value != command.get("value"): if command.get("attribute") == point.attribute: temp_index = point.index - point.value = int(command.get("value")) + point.value =float(command.get("value")) + # point.value = int(command.get("value")) print("PV ",point.index, point.value, point.attribute) - master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), + # master.send_direct_operate_command(opendnp3.AnalogOutputFloat32(point.value), + master.send_direct_operate_command(opendnp3.AnalogOutputDouble64(point.value), temp_index, command_callback) diff --git a/test/master_main.py b/test/master_main.py index 96560db..5135d3d 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -14,6 +14,7 @@ from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication from dnp3.dnp3_to_cim import CIMMapping from pydnp3 import opendnp3, openpal +from dnp3.points import PointValue from gridappsd.topics import simulation_output_topic, simulation_input_topic from gridappsd import GridAPPSD, DifferenceBuilder, utils @@ -79,15 +80,12 @@ def run_master(device_ip_port_config_all, names): conversion_dict=os.path.join(data_loc,"conversion_dict_master.json") with open(conversion_dict) as f: conversion_dict = json.load(f) - # print(conversion_dict['RTU1']['Analog output']) - from dnp3.points import PointValue - - - pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) - pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" - pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) - pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" - pv_points = [pv_point_tmp1,pv_point_tmp2] + + # pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) + # pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + # pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) + # pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + # pv_points = [pv_point_tmp1,pv_point_tmp2] pv_points =[] for k,v in conversion_dict['RTU1']['Analog output'].items(): if 'CIM mRID' in v: @@ -123,9 +121,6 @@ def run_master(device_ip_port_config_all, names): # master.fast_scan_all.Demand() msg_count=0 csv_dict = {} - for master in masters: - rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'.csv', list(range(300))) - csv_dict[master.name] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} while True: # cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} @@ -133,9 +128,13 @@ def run_master(device_ip_port_config_all, names): print("getting CIM from master "+master.name) cim_msg = master.soe_handler.get_msg() dnp3_msg = master.soe_handler.get_dnp3_msg() + # print('keys',list(dnp3_msg.keys())) + if master.name not in csv_dict: + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'.csv', list(dnp3_msg.keys())) + csv_dict[master.name] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} if len(dnp3_msg.keys()) > 0: - max_index = max(dnp3_msg.keys()) - values = [dnp3_msg[k] for k in range(max_index)] + # max_index = max(dnp3_msg.keys()) + values = [dnp3_msg[k] for k in dnp3_msg.keys()] csv_dict[master.name]['csv_writer'].writerow(np.insert(values,0, msg_count)) csv_dict[master.name]['csv_file'].flush() else: @@ -149,7 +148,7 @@ def run_master(device_ip_port_config_all, names): print(master.name+" " +str(cim_full_msg)[:100]) _log.info(cim_full_msg) - time.sleep(5) + time.sleep(10) print('\nStopping') @@ -186,6 +185,10 @@ def run_master(device_ip_port_config_all, names): for device_name in device_names: if 'RTU' in device_name: names.append(device_name) + if 'shark' in device_name[:5]: + names.append(device_name) + if 'capbank' in device_name: + names.append(device_name) print("Running "+ str(names)) time.sleep(2) From e56db066039cc42f520653b4e53122974ac4f602 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Tue, 16 Mar 2021 18:17:37 -0600 Subject: [PATCH 55/64] Added RTU handling for binary --- dnp3/service/dnp3/master.py | 65 ++++++++++++++++++++++++++++++++----- test/CIMProcessor.py | 24 +++++++------- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 92b038f..c9740c0 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -374,14 +374,45 @@ def update_cim_msg_analog(self, CIM_msg, index, value, conversion, model): CIM_msg[mrid] = CIM_value CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier + def update_cim_msg_binary_rtu(self, CIM_msg, index, value, conversion,model): + # print(conversion['Binary input'][index]) + print('binary jeff', model) + + CIM_phases = conversion[index]['CIM phase'] + CIM_units = conversion[index]['CIM units'] + CIM_attribute = conversion[index]['CIM attribute'] + print('binary phases',CIM_phases, CIM_units) + ## Check if multiplier is na or str + # multiplier = conversion['Binary input'][index]['Multiplier'] + + print(type(CIM_units),CIM_units) + if CIM_units not in model: + print(str(CIM_units) +' not in model') + return + for CIM_phase in CIM_phases: + # print(model) + # exit(0) + mrid = model[CIM_units][CIM_phase]['mrid'] + CIM_value = {'mrid': mrid} + if mrid not in CIM_msg: + CIM_msg[mrid] = CIM_value + int_value = 1 + if value: + int_value=0 + CIM_msg[mrid][CIM_attribute] = int_value + print('binary',value, int_value) + def update_cim_msg_binary(self, CIM_msg, index, value, conversion,model): # print(conversion['Binary input'][index]) + print('binary jeff', model) if 'Binary input' in conversion and index in conversion['Binary input']: CIM_phases = conversion['Binary input'][index]['CIM phase'] CIM_units = conversion['Binary input'][index]['CIM units'] CIM_attribute = conversion['Binary input'][index]['CIM attribute'] + print('binary phases',CIM_phases) ## Check if multiplier is na or str - multiplier = conversion['Binary input'][index]['Multiplier'] + # multiplier = conversion['Binary input'][index]['Multiplier'] + print(type(CIM_units),CIM_units) if CIM_units not in model: print(str(CIM_units) +' not in model') @@ -393,7 +424,8 @@ def update_cim_msg_binary(self, CIM_msg, index, value, conversion,model): CIM_value = {'mrid': mrid} if mrid not in CIM_msg: CIM_msg[mrid] = CIM_value - CIM_msg[mrid][CIM_attribute] = value * multiplier # times multiplier + CIM_msg[mrid][CIM_attribute] = value + print('binary',value) def Process(self, info, values): """ @@ -444,6 +476,7 @@ def Process(self, info, values): else: _log.debug("No conversion for " + str(index)) elif isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: + self._dnp3_msg[index]=value self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) # ## Check if multiplier is na or str # multiplier = 1 @@ -464,17 +497,31 @@ def Process(self, info, values): # value_type = element_attr_to_mrid[dnp3_attr_name]['type'] # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier elif str(index) in conversion['Analog input']: + self._dnp3_msg[index]=value self.update_cim_msg_analog(self._cim_msg, str(index), value, conversion, model) else: print(" No entry for index " + str(index)) # print(cim_msg) ## '/topic/goss.gridappsd.fim.input' - elif type(values) == opendnp3.ICollectionIndexedBinaryOutputStatus: - for index, value in visitor.index_and_value: - print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) - # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work + elif type(values) == opendnp3.ICollectionIndexedBinary: + if 'RTU' in self._device: + print('Jeff Binary RTU') + for index, value in visitor.index_and_value: + self._dnp3_msg[index]=value + conversion_name_index_dict = {v['index']: v for k, v in conversion['Binary input'].items()} + if index in conversion_name_index_dict: + # _log.debug("Conversion for " + str(index)) + model = model_line_dict[conversion_name_index_dict[index]['CIM name']] + # self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) + self.update_cim_msg_binary_rtu(self._cim_msg, index, value, conversion_name_index_dict, model) + else: + _log.debug("No conversion for " + str(index)) + else: + for index, value in visitor.index_and_value: + print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work else: for index, value in visitor.index_and_value: print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) @@ -485,7 +532,7 @@ def Process(self, info, values): # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) # self._cim_msg = {"test":time.time()} - print(str(self._cim_msg)[:80]) + print(str(self._cim_msg)[:200]) def Start(self): _log.debug('In SOEHandler.Start') diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index 53b59d9..c267d7a 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -61,9 +61,9 @@ def process(self, message): cap_point1 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) cap_point1.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" - cap_point2 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point2 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=1, op_type=None) cap_point2.measurement_id = "_C1706031-2C1C-464C-8376-6A51FA70B470" - cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=2, op_type=None) cap_point3.measurement_id = "_245E3924-8292-46D5-A11E-C80F7D6EE253" cap_list = [cap_point1,cap_point2,cap_point3] reg_point1 = PointValue(command_type=None, function_code=None, value=100, point_def=0, index=0, op_type=None) @@ -101,14 +101,14 @@ def process(self, message): if open_cmd: # Open master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 0, # PULSE/LATCH_ON to index 0 for open - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 1, # PULSE/LATCH_ON to index 1 for open - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - 2, # PULSE/LATCH_ON to index 2 for open + point.index, # PULSE/LATCH_ON to index 0 for open command_callback) + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 1, # PULSE/LATCH_ON to index 1 for open + # command_callback) + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 2, # PULSE/LATCH_ON to index 2 for open + # command_callback) point.value = 0 else: # Will need 5 minutes after open operation for this capbank @@ -147,11 +147,11 @@ def process(self, message): if command.get("attribute") == point.attribute: temp_index = point.index point.value =float(command.get("value")) - # point.value = int(command.get("value")) + point.value = int(command.get("value")) print("PV ",point.index, point.value, point.attribute) - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), + master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), # master.send_direct_operate_command(opendnp3.AnalogOutputFloat32(point.value), - master.send_direct_operate_command(opendnp3.AnalogOutputDouble64(point.value), + # master.send_direct_operate_command(opendnp3.AnalogOutputDouble64(point.value), temp_index, command_callback) From 7869456ab5c6396c5b11751dc7df30834b15833c Mon Sep 17 00:00:00 2001 From: jsimpson Date: Thu, 18 Mar 2021 16:00:53 -0600 Subject: [PATCH 56/64] Added header, AI and thread locks --- dnp3/service/dnp3/master.py | 164 +++++++++++++++++++++--------------- test/master_main.py | 32 ++++--- test/outstation.py | 7 +- test/outstation_main.py | 37 +++++--- 4 files changed, 145 insertions(+), 95 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index c9740c0..493852b 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -39,6 +39,7 @@ import yaml import json import numpy as np +import threading from gridappsd.topics import simulation_output_topic, simulation_input_topic from gridappsd import GridAPPSD, DifferenceBuilder, utils @@ -311,15 +312,30 @@ def __init__(self, name, device, dnp3_to_cim): # username=utils.get_gridappsd_user(), password=utils.get_gridappsd_pass()) # self._gapps.subscribe('/topic/goss.gridappsd.fim.input.'+str(1234), on_message) self._cim_msg = {} - self._dnp3_msg = {} + self._dnp3_msg_AI = {} + self._dnp3_msg_AI_header = [] + self._dnp3_msg_BI = {} + self._dnp3_msg_BI_header = [] + self.lock = threading.Lock() super(SOEHandler, self).__init__() def get_msg(self): - return self._cim_msg + with self.lock: + # time.sleep(2) + return self._cim_msg - def get_dnp3_msg(self): - return self._dnp3_msg + def get_dnp3_msg_AI(self): + with self.lock: + return self._dnp3_msg_AI + + def get_dnp3_msg_AI_header(self): + with self.lock: + return self._dnp3_msg_AI_header + + def get_dnp3_msg_BI(self): + with self.lock: + return self._dnp3_msg_BI def update_cim_msg_analog_multi_index(self, CIM_msg, index, value, conversion, model): CIM_phase = conversion[index]['CIM phase'] @@ -455,84 +471,94 @@ def Process(self, info, values): element_attr_to_mrid = model_line_dict[self._name] model = model_line_dict[self._name] conversion = conversion_dict[self._device] + conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} # print(conversion) ## TODO check for each type seperate binary vs analog ## Analog check - print(type(values)) - if type(values) == opendnp3.ICollectionIndexedAnalog: - for index, value in visitor.index_and_value: - print("Jeff SOE", index, value, isinstance(value, numbers.Number)) - # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - if 'RTU' in self._device: - print('Jeff RTU') - self._dnp3_msg[index]=value - conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} - if index in conversion_name_index_dict: - # _log.debug("Conversion for " + str(index)) - model = model_line_dict[conversion_name_index_dict[index]['CIM name']] - self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) - else: - _log.debug("No conversion for " + str(index)) - elif isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: - self._dnp3_msg[index]=value - self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) - # ## Check if multiplier is na or str - # multiplier = 1 - # conversion_for_index = conversion['Analog input'][str(float(index))] - # if 'Multiplier' in conversion_for_index: - # multiplier = conversion_for_index['Multiplier'] - # if type(multiplier) != str: - # multiplier = conversion_for_index['Multiplier'] - # else: - # print("No multiplier") - # dnp3_attr_name = conversion_for_index['type'] - # if dnp3_attr_name in element_attr_to_mrid: - # # print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) - # if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in self._cim_msg: - # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, - # 'angle': 0, - # 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} - # value_type = element_attr_to_mrid[dnp3_attr_name]['type'] - # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier - elif str(index) in conversion['Analog input']: - self._dnp3_msg[index]=value - self.update_cim_msg_analog(self._cim_msg, str(index), value, conversion, model) - else: - print(" No entry for index " + str(index)) - # print(cim_msg) - ## '/topic/goss.gridappsd.fim.input' - elif type(values) == opendnp3.ICollectionIndexedBinary: - if 'RTU' in self._device: - print('Jeff Binary RTU') + with self.lock: + print(type(values)) + print(visitor) + if type(values) == opendnp3.ICollectionIndexedAnalog: + print(dir(values)) + # values.ForeachItem() for index, value in visitor.index_and_value: - self._dnp3_msg[index]=value - conversion_name_index_dict = {v['index']: v for k, v in conversion['Binary input'].items()} - if index in conversion_name_index_dict: - # _log.debug("Conversion for " + str(index)) - model = model_line_dict[conversion_name_index_dict[index]['CIM name']] - # self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) - self.update_cim_msg_binary_rtu(self._cim_msg, index, value, conversion_name_index_dict, model) + print("Jeff SOE IndexedAnalog", index, value, isinstance(value, numbers.Number)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + if not self._dnp3_msg_AI_header: + # self._dnp3_msg_AI_header = {v['index']: v['CIM name'] for k, v in conversion['Analog input'].items()} + self._dnp3_msg_AI_header = [v['CIM name']+'_'+v['CIM units'] for k, v in conversion['Analog input'].items()] + if 'RTU' in self._device: + print('Jeff RTU') + self._dnp3_msg_AI[index]=value + + if index in conversion_name_index_dict: + # _log.debug("Conversion for " + str(index)) + model = model_line_dict[conversion_name_index_dict[index]['CIM name']] + self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) + else: + _log.debug("No conversion for " + str(index)) + print("No conversion for " + str(index)) + elif isinstance(value, numbers.Number) and str(float(index)) in conversion['Analog input']: + self._dnp3_msg_AI[index]=value + self.update_cim_msg_analog(self._cim_msg, str(float(index)), value, conversion, model) + # ## Check if multiplier is na or str + # multiplier = 1 + # conversion_for_index = conversion['Analog input'][str(float(index))] + # if 'Multiplier' in conversion_for_index: + # multiplier = conversion_for_index['Multiplier'] + # if type(multiplier) != str: + # multiplier = conversion_for_index['Multiplier'] + # else: + # print("No multiplier") + # dnp3_attr_name = conversion_for_index['type'] + # if dnp3_attr_name in element_attr_to_mrid: + # # print(dnp3_attr_name,element_attr_to_mrid[dnp3_attr_name]) + # if element_attr_to_mrid[dnp3_attr_name]['mrid'] not in self._cim_msg: + # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']] = {'magnitude': 0, + # 'angle': 0, + # 'mrid': element_attr_to_mrid[dnp3_attr_name]['mrid']} + # value_type = element_attr_to_mrid[dnp3_attr_name]['type'] + # self._cim_msg[element_attr_to_mrid[dnp3_attr_name]['mrid']][value_type] = value * multiplier + elif str(index) in conversion['Analog input']: + self._dnp3_msg_AI[index]=value + self.update_cim_msg_analog(self._cim_msg, str(index), value, conversion, model) else: - _log.debug("No conversion for " + str(index)) + print(" No entry for index " + str(index)) + # print(cim_msg) + ## '/topic/goss.gridappsd.fim.input' + elif type(values) == opendnp3.ICollectionIndexedBinary: + if 'RTU' in self._device and 'Binary input' in conversion: + print('Jeff Binary RTU') + for index, value in visitor.index_and_value: + self._dnp3_msg_BI[index]=value + conversion_name_index_dict = {v['index']: v for k, v in conversion['Binary input'].items()} + if index in conversion_name_index_dict: + # _log.debug("Conversion for " + str(index)) + model = model_line_dict[conversion_name_index_dict[index]['CIM name']] + # self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) + self.update_cim_msg_binary_rtu(self._cim_msg, index, value, conversion_name_index_dict, model) + else: + _log.debug("No conversion for " + str(index)) + else: + for index, value in visitor.index_and_value: + print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) + # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' + # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) + self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work else: for index, value in visitor.index_and_value: - print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) + print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work - else: - for index, value in visitor.index_and_value: - print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) - # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' - # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) - # self._cim_msg = cim_msg - # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) + # self._cim_msg = cim_msg + # self._gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(self._cim_msg)) - # self._cim_msg = {"test":time.time()} + # self._cim_msg = {"test":time.time()} print(str(self._cim_msg)[:200]) + def Start(self): _log.debug('In SOEHandler.Start') diff --git a/test/master_main.py b/test/master_main.py index 5135d3d..703340f 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -11,7 +11,7 @@ from CIMProcessor import CIMProcessor -from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, SOEHandlerSimple, MasterApplication from dnp3.dnp3_to_cim import CIMMapping from pydnp3 import opendnp3, openpal from dnp3.points import PointValue @@ -29,8 +29,8 @@ def build_csv_writers(folder, filename, column_names): os.remove(_file) file_handle = open(_file, 'a') csv_writer = csv.writer(file_handle, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) - csv_writer.writerow(column_names) - # csv_writer.writerow(['epoch time'] + column_names) + # csv_writer.writerow(column_names) + csv_writer.writerow(['time'] + column_names) return file_handle, csv_writer def on_message(simulation_id, message): @@ -70,6 +70,7 @@ def run_master(device_ip_port_config_all, names): log_handler=MyLogger(), listener=AppChannelListener(), soe_handler=SOEHandler(object_name, convertion_type, dnp3_to_cim), + # soe_handler=SOEHandlerSimple(), master_application=MasterApplication()) application_1.name=name # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) @@ -86,13 +87,14 @@ def run_master(device_ip_port_config_all, names): # pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) # pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" # pv_points = [pv_point_tmp1,pv_point_tmp2] - pv_points =[] - for k,v in conversion_dict['RTU1']['Analog output'].items(): - if 'CIM mRID' in v: - pv_point = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=int(v['index']), op_type=None) - pv_point.measurement_id = v['CIM mRID'] - pv_point.attribute = v['CIM attribute'] - pv_points.append(pv_point) + pv_points = [] + if 'RTU1' in conversion_dict and 'Analog output' in conversion_dict['RTU1']: + for k, v in conversion_dict['RTU1']['Analog output'].items(): + if 'CIM mRID' in v: + pv_point = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=int(v['index']), op_type=None) + pv_point.measurement_id = v['CIM mRID'] + pv_point.attribute = v['CIM attribute'] + pv_points.append(pv_point) # just build points for PV @@ -121,17 +123,18 @@ def run_master(device_ip_port_config_all, names): # master.fast_scan_all.Demand() msg_count=0 csv_dict = {} - cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} + cim_full_msg = {'simulation_id': 1234, 'timestamp': str(int(time.time())), 'message':{'measurements':{}}} while True: # cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} for master in masters: print("getting CIM from master "+master.name) cim_msg = master.soe_handler.get_msg() - dnp3_msg = master.soe_handler.get_dnp3_msg() + dnp3_msg = master.soe_handler.get_dnp3_msg_AI() # print('keys',list(dnp3_msg.keys())) if master.name not in csv_dict: - rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'.csv', list(dnp3_msg.keys())) + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_AI.csv', list(dnp3_msg.keys())) csv_dict[master.name] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} + csv_dict[master.name]['csv_writer'].writerow(['time']+master.soe_handler.get_dnp3_msg_AI_header()) if len(dnp3_msg.keys()) > 0: # max_index = max(dnp3_msg.keys()) values = [dnp3_msg[k] for k in dnp3_msg.keys()] @@ -143,12 +146,13 @@ def run_master(device_ip_port_config_all, names): # print(cim_msg) # message['message']['measurements'] cim_full_msg['message']['measurements'].update(cim_msg) + cim_full_msg['timestamp'] = str(int(time.time())) gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_full_msg)) msg_count+=1 print(master.name+" " +str(cim_full_msg)[:100]) _log.info(cim_full_msg) - time.sleep(10) + time.sleep(1) print('\nStopping') diff --git a/test/outstation.py b/test/outstation.py index cd4f1fd..0b5d68d 100644 --- a/test/outstation.py +++ b/test/outstation.py @@ -46,15 +46,18 @@ class OutstationApplication(opendnp3.IOutstationApplication): def __init__(self, LOCAL_IP="0.0.0.0", PORT=20000, - DNP3_ADDR=1, config={}): + DNP3_ADDR=1, + LocalAddr=1, + config={}): self.DNP3_ADDR = DNP3_ADDR + self.LocalAddr= LocalAddr self.config = config super(OutstationApplication, self).__init__() _log.debug('Configuring the DNP3 stack.') self.stack_config = self.configure_stack() self.stack_config.link.LocalAddr = self.DNP3_ADDR ## Was 10 - # self.stack_config.link.RemoteAddr = 1 ## TODO + # self.stack_config.link.RemoteAddr = self.LocalAddr ## TODO _log.debug('Configuring the outstation database.') self.configure_database(self.stack_config.dbConfig) diff --git a/test/outstation_main.py b/test/outstation_main.py index b18b907..b68585b 100644 --- a/test/outstation_main.py +++ b/test/outstation_main.py @@ -1,8 +1,9 @@ import os import sys import time +import platform -from pydnp3 import opendnp3 +from pydnp3 import opendnp3, asiodnp3 sys.path.append("../dnp3/service") from dnp3.dnp3_to_cim import CIMMapping @@ -12,12 +13,18 @@ # def run_master(HOST="127.0.0.1",PORT=20000, DNP3_ADDR=10, convertion_type='Shark', object_name='632633'): def run_outstation(device_ip_port_config_all, names): masters = [] - dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict.json", model_line_dict="model_line_dict.json") + data_loc = '.' + if 'Darwin' != platform.system(): + data_loc = '/media/sf_git_scada_read/GridAppsD_Usecase3/701_disag/701/test' + # dnp3_to_cim = CIMMapping(conversion_dict="conversion_dict_master.json", model_line_dict="model_line_dict.json") + dnp3_to_cim = CIMMapping(conversion_dict=os.path.join(data_loc, "conversion_dict_master.json"), + model_line_dict=os.path.join(data_loc, "model_line_dict_master.json")) for name in names: device_ip_port_dict = device_ip_port_config_all[name] HOST=device_ip_port_dict['ip'] PORT=device_ip_port_dict['port'] DNP3_ADDR= device_ip_port_dict['link_local_addr'] + LocalAddr= device_ip_port_dict['link_remote_addr'] convertion_type=device_ip_port_dict['conversion_type'] object_name='632633' @@ -25,21 +32,31 @@ def run_outstation(device_ip_port_config_all, names): application_1 = OutstationApplication( LOCAL_IP="0.0.0.0", PORT=int(PORT), - DNP3_ADDR=int(DNP3_ADDR), config=device_ip_port_dict) + DNP3_ADDR=int(DNP3_ADDR), + LocalAddr=LocalAddr, + config=device_ip_port_dict) # application.channel.SetLogFilters(openpal.LogFilters(opendnp3.levels.ALL_COMMS)) # print('Channel log filtering level is now: {0}'.format(opendnp3.levels.ALL_COMMS)) masters.append(application_1) - + builder = asiodnp3.UpdateBuilder() while True: # cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'messages':{}} for master in masters: - master.config["conversion_type"] - for index, value in dnp3_to_cim.conversion_dict[master.config["conversion_type"]]['Analog input'].items(): - print(index) - int(float(index)) - master.apply_update(opendnp3.Analog(1000.3 + float(index)), int(float(index))) - time.sleep(10) + print('Conversion type',master.config["conversion_type"]) + if 'RTU' in master.config["conversion_type"]: + print('RTU',list(dnp3_to_cim.conversion_dict[master.config["conversion_type"]]['Analog input'].keys())) + for index, value in dnp3_to_cim.conversion_dict[master.config["conversion_type"]]['Analog input'].items(): + # for index in range(100): + # master.apply_update(opendnp3.Analog(101.1223 + 2*float(index)), int(float(index))) + builder.Update(opendnp3.Analog(101.1223 + 2*float(index)), int(index)) + # else: + # for index, value in dnp3_to_cim.conversion_dict[master.config["conversion_type"]]['Analog input'].items(): + # master.apply_update(opendnp3.Analog(1000.3 + 2*float(index)), int(float(index))) + print("Updates Created") + updates = builder.Build() + OutstationApplication.get_outstation().Apply(updates) + time.sleep(1) print('\nStopping') for master in masters: From f35d6f212e15712ed5d2524349c018806bc1d9ae Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 19 Mar 2021 14:26:00 -0600 Subject: [PATCH 57/64] Updated for saving data --- .gitignore | 227 ++++++++++++----------- dnp3/service/dnp3/master.py | 15 +- test/CIMProcessor.py | 361 ++++++++++++++++++++---------------- test/master_main.py | 52 ++++-- 4 files changed, 367 insertions(+), 288 deletions(-) diff --git a/.gitignore b/.gitignore index 48751fc..1343812 100644 --- a/.gitignore +++ b/.gitignore @@ -1,111 +1,116 @@ - -\.idea/ -docs/build -env/ -*.swp -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -pydnp3/dnp3/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# dotenv -.env - -# virtualenv -.venv -venv/ -ENV/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -*.pytest_cache/ - -# VisualStudio and VisualStudio Code project settings -.vscode/ -.vs/ + +\.idea/ +docs/build +env/ +*.swp +__pycache__/ +*.py[cod] +*$py.class + + +*.csv +*.json +*.save + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +pydnp3/dnp3/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +*.pytest_cache/ + +# VisualStudio and VisualStudio Code project settings +.vscode/ +.vs/ diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 493852b..f8bedb4 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -337,6 +337,10 @@ def get_dnp3_msg_BI(self): with self.lock: return self._dnp3_msg_BI + def get_dnp3_msg_BI_header(self): + with self.lock: + return self._dnp3_msg_BI_header + def update_cim_msg_analog_multi_index(self, CIM_msg, index, value, conversion, model): CIM_phase = conversion[index]['CIM phase'] CIM_units = conversion[index]['CIM units'] @@ -471,7 +475,11 @@ def Process(self, info, values): element_attr_to_mrid = model_line_dict[self._name] model = model_line_dict[self._name] conversion = conversion_dict[self._device] - conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} + print(self._device) + if self._device == 'Shark': + conversion_name_index_dict = {v['Index']: v for k, v in conversion['Analog input'].items()} + else: + conversion_name_index_dict = {v['index']: v for k, v in conversion['Analog input'].items()} # print(conversion) ## TODO check for each type seperate binary vs analog @@ -488,7 +496,10 @@ def Process(self, info, values): # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) if not self._dnp3_msg_AI_header: # self._dnp3_msg_AI_header = {v['index']: v['CIM name'] for k, v in conversion['Analog input'].items()} - self._dnp3_msg_AI_header = [v['CIM name']+'_'+v['CIM units'] for k, v in conversion['Analog input'].items()] + if self._device == 'Shark': + self._dnp3_msg_AI_header = [v['Type'] for k, v in conversion['Analog input'].items()] + else: + self._dnp3_msg_AI_header = [v['CIM name']+'_'+v['CIM units'] for k, v in conversion['Analog input'].items()] if 'RTU' in self._device: print('Jeff RTU') self._dnp3_msg_AI[index]=value diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index c267d7a..a179795 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -1,164 +1,197 @@ - -from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication, SOEHandlerSimple -from dnp3.dnp3_to_cim import CIMMapping -from pydnp3 import opendnp3, openpal -import yaml -from dnp3.points import PointValue - -def collection_callback(result=None): - """ - :type result: opendnp3.CommandPointResult - """ - print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( - result.headerIndex, - result.index, - opendnp3.CommandPointStateToString(result.state), - opendnp3.CommandStatusToString(result.status) - )) - # print(result) - - -def command_callback(result=None): - """ - :type result: opendnp3.ICommandTaskResult - """ - print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) - result.ForeachItem(collection_callback) - -class CIMProcessor(object): - - def __init__(self, point_definitions,master): - self.point_definitions = point_definitions # TODO - self._master = master - self._pv_points = point_definitions - - def process(self, message): - master = self._master - # print("Message received:", simulation_id['message-id']) - print(message) - json_msg = yaml.safe_load(str(message)) - # self.master.apply_update(opendnp3.Binary(0), 12) - - # self.master.send_direct_operate_command( - # opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 5, - # command_callback) - - if type(json_msg) != dict: - raise ValueError( - ' is not a json formatted string.' - + '\njson_msg = {0}'.format(json_msg)) - - # fncs_input_message = {"{}".format(simulation_id): {}} -# "capacitors":[ -# {"name":"701-104cf","mRID":"_5955BE75-5EE5-477A-936F-65EDE5E3B831","CN1":"cf_701_311","phases":"ABC","kvar_A":400.0,"kvar_B":400.0,"kvar_C":400.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_70799_mc","monitoredClass":"ACLineSegment","monitoredBus":"fu_701_9492","monitoredPhase":"B"}, -# {"name":"701-275cw","mRID":"_C1706031-2C1C-464C-8376-6A51FA70B470","CN1":"fu_701_20042","phases":"ABC","kvar_A":100.0,"kvar_B":100.0,"kvar_C":100.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_167489_mc","monitoredClass":"ACLineSegment","monitoredBus":"cw_701_1710","monitoredPhase":"B"}, -# {"name":"701-319cw","mRID":"_245E3924-8292-46D5-A11E-C80F7D6EE253","CN1":"cw_701_347","phases":"ABC","kvar_A":400.0,"kvar_B":400.0,"kvar_C":400.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_57838_mc","monitoredClass":"ACLineSegment","monitoredBus":"fu_701_9026","monitoredPhase":"B"} -# ], -# "regulators":[ -# {"bankName":"s1","size":"1","bankPhases":"ABC","tankName":[""],"endNumber":[2],"endPhase":["ABC"],"rtcName":["s1"],"mRID":["_D081C22C-D840-4303-8C95-C9151610C9A6"],"monitoredPhase":["A"],"TapChanger.tculControlMode":["volt"],"highStep":[16],"lowStep":[-16],"neutralStep":[0],"normalStep":[0],"TapChanger.controlEnabled":[true],"lineDropCompensation":[true],"ltcFlag":[true],"RegulatingControl.enabled":[true],"RegulatingControl.discrete":[true],"RegulatingControl.mode":["voltage"],"step":[-8],"targetValue":[123.0000],"targetDeadband":[2.0000],"limitVoltage":[0.0000],"stepVoltageIncrement":[0.6250],"neutralU":[6927.6000],"initialDelay":[30.0000],"subsequentDelay":[2.0000],"lineDropR":[7.0000],"lineDropX":[0.0000],"reverseLineDropR":[0.0000],"reverseLineDropX":[0.0000],"ctRating":[300.0000],"ctRatio":[1500.0000],"ptRatio":[57.7300]} -# ], - - cap_point1 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) - cap_point1.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" - cap_point2 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=1, op_type=None) - cap_point2.measurement_id = "_C1706031-2C1C-464C-8376-6A51FA70B470" - cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=2, op_type=None) - cap_point3.measurement_id = "_245E3924-8292-46D5-A11E-C80F7D6EE253" - cap_list = [cap_point1,cap_point2,cap_point3] - reg_point1 = PointValue(command_type=None, function_code=None, value=100, point_def=0, index=0, op_type=None) - reg_point1.measurement_id = "_D081C22C-D840-4303-8C95-C9151610C9A6" - reg_list = [reg_point1] - # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" - # master = masters[0] - - # {"command": "update", "input": {"simulation_id": "1312790133", "message": {"timestamp": 1614730782, "difference_mrid": "e8287b64-6802-49b5-a6f0-12d8048fd98e", "reverse_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}], "forward_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}]}}} - - # print(json_msg) - # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} - if "input" in json_msg.keys(): - print("input Jeff") - control_values = json_msg["input"]["message"]["forward_differences"] - print(control_values) - - for command in control_values: - print("command", command) - # master = self.master_dict[command["object"]] - # print(master) - for point in reg_list: - if command.get("object") == point.measurement_id and point.value != command.get("value"): - point.value = command.get("value") - print("Send reg value "+ str(command.get("value"))) - temp_value = int(command.get("value")) - master.send_direct_operate_command(opendnp3.AnalogOutputInt32(temp_value), - 0, - command_callback) - - for point in cap_list: - # Capbank - if command.get("object") == point.measurement_id and point.value != command.get("value"): - open_cmd = command.get("value") == 0 - if open_cmd: - # Open - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - point.index, # PULSE/LATCH_ON to index 0 for open - command_callback) - # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 1, # PULSE/LATCH_ON to index 1 for open - # command_callback) - # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 2, # PULSE/LATCH_ON to index 2 for open - # command_callback) - point.value = 0 - else: - # Will need 5 minutes after open operation for this capbank - # Close - # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 0, # PULSE/LATCH_ON to index 0 for close - # command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 0, # PULSE/LATCH_ON to index 0 for close - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 1, # PULSE/LATCH_ON to index 0 for close - command_callback) - master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), - 2, # PULSE/LATCH_ON to index 0 for close - command_callback) - point.value = 1 - elif command.get("object") == point.measurement_id and point.value == command.get("value"): - print("Cap check", command.get("object"), command.get("value")) - - pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) - pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" - pv_point_tmp1.attribute='PowerElectronicsConnection.p' -# command {'object': '_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B', 'attribute': 'PowerElectronicsConnection.p', 'value': 17.27693169381238} -# PV -# PV -# command {'object': '_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B', 'attribute': 'PowerElectronicsConnection.q', 'value': 41.028701716375124} - # pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" - pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) - pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" - pv_point_tmp2.attribute='PowerElectronicsConnection.q' - pv_points=[pv_point_tmp1,pv_point_tmp2] - # PV points - for point in self._pv_points: - if command.get("object") == point.measurement_id and point.value != command.get("value"): - if command.get("attribute") == point.attribute: - temp_index = point.index - point.value =float(command.get("value")) - point.value = int(command.get("value")) - print("PV ",point.index, point.value, point.attribute) - master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), - # master.send_direct_operate_command(opendnp3.AnalogOutputFloat32(point.value), - # master.send_direct_operate_command(opendnp3.AnalogOutputDouble64(point.value), - temp_index, - command_callback) - - # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), - # 2, - # command_callback) - - # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), - # 1, - # command_callback) \ No newline at end of file + +from dnp3.master import MyMaster, MyLogger, AppChannelListener, SOEHandler, MasterApplication, SOEHandlerSimple +from dnp3.dnp3_to_cim import CIMMapping +from pydnp3 import opendnp3, openpal +import yaml +from dnp3.points import PointValue +import threading + +def collection_callback(result=None): + """ + :type result: opendnp3.CommandPointResult + """ + print("Header: {0} | Index: {1} | State: {2} | Status: {3}".format( + result.headerIndex, + result.index, + opendnp3.CommandPointStateToString(result.state), + opendnp3.CommandStatusToString(result.status) + )) + # print(result) + + +def command_callback(result=None): + """ + :type result: opendnp3.ICommandTaskResult + """ + print("Received command result with summary: {}".format(opendnp3.TaskCompletionToString(result.summary))) + result.ForeachItem(collection_callback) + +class CIMProcessor(object): + + def __init__(self, point_definitions,master): + self.point_definitions = point_definitions # TODO + self._master = master + self._pv_points = point_definitions + self._dnp3_msg_AO = {} + self._dnp3_msg_BO = {} + self._dnp3_msg_AO_header = [] + self._dnp3_msg_BO_header = [] + self.lock = threading.Lock() + + + def get_dnp3_msg_AO(self): + with self.lock: + return self._dnp3_msg_AO + + def get_dnp3_msg_AO_header(self): + with self.lock: + return self._dnp3_msg_AO_header + + def get_dnp3_msg_BO(self): + with self.lock: + return self._dnp3_msg_BO + + def process(self, message): + master = self._master + # print("Message received:", simulation_id['message-id']) + print(message) + json_msg = yaml.safe_load(str(message)) + # self.master.apply_update(opendnp3.Binary(0), 12) + + # self.master.send_direct_operate_command( + # opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 5, + # command_callback) + + if type(json_msg) != dict: + raise ValueError( + ' is not a json formatted string.' + + '\njson_msg = {0}'.format(json_msg)) + + # fncs_input_message = {"{}".format(simulation_id): {}} +# "capacitors":[ +# {"name":"701-104cf","mRID":"_5955BE75-5EE5-477A-936F-65EDE5E3B831","CN1":"cf_701_311","phases":"ABC","kvar_A":400.0,"kvar_B":400.0,"kvar_C":400.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_70799_mc","monitoredClass":"ACLineSegment","monitoredBus":"fu_701_9492","monitoredPhase":"B"}, +# {"name":"701-275cw","mRID":"_C1706031-2C1C-464C-8376-6A51FA70B470","CN1":"fu_701_20042","phases":"ABC","kvar_A":100.0,"kvar_B":100.0,"kvar_C":100.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_167489_mc","monitoredClass":"ACLineSegment","monitoredBus":"cw_701_1710","monitoredPhase":"B"}, +# {"name":"701-319cw","mRID":"_245E3924-8292-46D5-A11E-C80F7D6EE253","CN1":"cw_701_347","phases":"ABC","kvar_A":400.0,"kvar_B":400.0,"kvar_C":400.0,"nominalVoltage":12000.0,"nomU":12000.0,"phaseConnection":"Y","grounded":true,"enabled":true,"mode":"voltage","targetValue":7080.5,"targetDeadband":57.8,"aVRDelay":0.0,"monitoredName":"701_57838_mc","monitoredClass":"ACLineSegment","monitoredBus":"fu_701_9026","monitoredPhase":"B"} +# ], +# "regulators":[ +# {"bankName":"s1","size":"1","bankPhases":"ABC","tankName":[""],"endNumber":[2],"endPhase":["ABC"],"rtcName":["s1"],"mRID":["_D081C22C-D840-4303-8C95-C9151610C9A6"],"monitoredPhase":["A"],"TapChanger.tculControlMode":["volt"],"highStep":[16],"lowStep":[-16],"neutralStep":[0],"normalStep":[0],"TapChanger.controlEnabled":[true],"lineDropCompensation":[true],"ltcFlag":[true],"RegulatingControl.enabled":[true],"RegulatingControl.discrete":[true],"RegulatingControl.mode":["voltage"],"step":[-8],"targetValue":[123.0000],"targetDeadband":[2.0000],"limitVoltage":[0.0000],"stepVoltageIncrement":[0.6250],"neutralU":[6927.6000],"initialDelay":[30.0000],"subsequentDelay":[2.0000],"lineDropR":[7.0000],"lineDropX":[0.0000],"reverseLineDropR":[0.0000],"reverseLineDropX":[0.0000],"ctRating":[300.0000],"ctRatio":[1500.0000],"ptRatio":[57.7300]} +# ], + + cap_point1 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point1.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + cap_point2 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=1, op_type=None) + cap_point2.measurement_id = "_C1706031-2C1C-464C-8376-6A51FA70B470" + cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=2, op_type=None) + cap_point3.measurement_id = "_245E3924-8292-46D5-A11E-C80F7D6EE253" + cap_list = [cap_point1,cap_point2,cap_point3] + reg_point1 = PointValue(command_type=None, function_code=None, value=100, point_def=0, index=0, op_type=None) + reg_point1.measurement_id = "_D081C22C-D840-4303-8C95-C9151610C9A6" + reg_list = [reg_point1] + # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + # master = masters[0] + + # {"command": "update", "input": {"simulation_id": "1312790133", "message": {"timestamp": 1614730782, "difference_mrid": "e8287b64-6802-49b5-a6f0-12d8048fd98e", "reverse_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 1}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}], "forward_differences": [{"object": "_5955BE75-5EE5-477A-936F-65EDE5E3B831", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_C1706031-2C1C-464C-8376-6A51FA70B470", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_245E3924-8292-46D5-A11E-C80F7D6EE253", "attribute": "ShuntCompensator.sections", "value": 0}, {"object": "_D081C22C-D840-4303-8C95-C9151610C9A6", "attribute": "TapChanger.step", "value": 0.0}]}}} + + # print(json_msg) + # received message {'command': 'update', 'input': {'simulation_id': '1764973334', 'message': {'timestamp': 1597447649, 'difference_mrid': '5ba5daf7-bf8b-4458-bc23-40ea3fb8078f', 'reverse_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 1}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 1}], 'forward_differences': [{'object': '_A9DE8829-58CB-4750-B2A2-672846A89753', 'attribute': 'ShuntCompensator.sections', 'value': 0}, {'object': '_9D725810-BFD6-44C6-961A-2BC027F6FC95', 'attribute': 'ShuntCompensator.sections', 'value': 0}]}}} + if "input" in json_msg.keys(): + print("input Jeff") + control_values = json_msg["input"]["message"]["forward_differences"] + print(control_values) + with self.lock: + for command in control_values: + print("command", command) + # master = self.master_dict[command["object"]] + # print(master) + self._dnp3_msg_AO = {} + self._dnp3_msg_BO = {} + for point in reg_list: + if command.get("object") == point.measurement_id : + self._dnp3_msg_AO[point.index] = point.value + if command.get("object") == point.measurement_id and point.value != command.get("value"): + point.value = command.get("value") + self._dnp3_msg_AO[point.index] = point.value + print("Send reg value "+ str(command.get("value"))) + temp_value = int(command.get("value")) + master.send_direct_operate_command(opendnp3.AnalogOutputInt32(temp_value), + point.index, + command_callback) + + for point in cap_list: + # Capbank + if command.get("object") == point.measurement_id : + self._dnp3_msg_BO[point.index] = point.value + if command.get("object") == point.measurement_id and point.value != command.get("value"): + open_cmd = command.get("value") == 0 + if open_cmd: + # Open + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + point.index, # PULSE/LATCH_ON to index 0 for open + command_callback) + self._dnp3_msg_BO[point.index] = point.value + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 1, # PULSE/LATCH_ON to index 1 for open + # command_callback) + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 2, # PULSE/LATCH_ON to index 2 for open + # command_callback) + point.value = 0 + else: + # Will need 5 minutes after open operation for this capbank + # Close + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 0, # PULSE/LATCH_ON to index 0 for close + # command_callback) + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + # 0, # PULSE/LATCH_ON to index 0 for close + # command_callback) + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + # 1, # PULSE/LATCH_ON to index 0 for close + # command_callback) + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + # 2, # PULSE/LATCH_ON to index 0 for close + # command_callback) + master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_OFF), + point.index, # PULSE/LATCH_ON to index 0 for open + command_callback) + self._dnp3_msg_BO[point.index] = point.value + + point.value = 1 + elif command.get("object") == point.measurement_id and point.value == command.get("value"): + print("Cap check", command.get("object"), command.get("value")) + + pv_point_tmp1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=1, op_type=None) + pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_point_tmp1.attribute='PowerElectronicsConnection.p' + # command {'object': '_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B', 'attribute': 'PowerElectronicsConnection.p', 'value': 17.27693169381238} + # PV + # PV + # command {'object': '_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B', 'attribute': 'PowerElectronicsConnection.q', 'value': 41.028701716375124} + # pv_point_tmp1.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_point_tmp2 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=2, op_type=None) + pv_point_tmp2.measurement_id = "_5D0562C7-FE25-4FEE-851E-8ADCD69CED3B" + pv_point_tmp2.attribute='PowerElectronicsConnection.q' + pv_points=[pv_point_tmp1,pv_point_tmp2] + # PV points + for point in self._pv_points: + if command.get("object") == point.measurement_id and point.value != command.get("value"): + if command.get("attribute") == point.attribute: + temp_index = point.index + point.value =float(command.get("value")) + point.value = int(command.get("value")) + self._dnp3_msg_AO[temp_index] = point.value + print("PV ",point.index, point.value, point.attribute) + master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), + # master.send_direct_operate_command(opendnp3.AnalogOutputFloat32(point.value), + # master.send_direct_operate_command(opendnp3.AnalogOutputDouble64(point.value), + temp_index, + command_callback) + + # master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), + # 2, + # command_callback) + + # master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), + # 1, + # command_callback) \ No newline at end of file diff --git a/test/master_main.py b/test/master_main.py index 703340f..b707296 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -129,17 +129,47 @@ def run_master(device_ip_port_config_all, names): for master in masters: print("getting CIM from master "+master.name) cim_msg = master.soe_handler.get_msg() - dnp3_msg = master.soe_handler.get_dnp3_msg_AI() + dnp3_msg_AI = master.soe_handler.get_dnp3_msg_AI() + dnp3_msg_AO = master.soe_handler.get_dnp3_msg_BI() + if master.name =='RTU1': + dnp3_msg_AO = myCIMProcessor.get_dnp3_msg_AO() + dnp3_msg_BO = myCIMProcessor.get_dnp3_msg_BO() # print('keys',list(dnp3_msg.keys())) - if master.name not in csv_dict: - rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_AI.csv', list(dnp3_msg.keys())) - csv_dict[master.name] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} - csv_dict[master.name]['csv_writer'].writerow(['time']+master.soe_handler.get_dnp3_msg_AI_header()) - if len(dnp3_msg.keys()) > 0: + if master.name+'AI' not in csv_dict: + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_AI.csv', list(dnp3_msg_AI.keys())) + csv_dict[master.name+'AI'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} + csv_dict[master.name+'AI']['csv_writer'].writerow(['time']+master.soe_handler.get_dnp3_msg_AI_header()) + if master.name+'BI' not in csv_dict: + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_BI.csv', list(dnp3_msg_AI.keys())) + csv_dict[master.name+'BI'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} + # csv_dict[master.name+'BI']['csv_writer'].writerow(['time']+master.soe_handler.get_dnp3_msg_BI_header()) + if master.name =='RTU1': + if master.name+'AO' not in csv_dict: + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_AO.csv', list(dnp3_msg_AO.keys())) + csv_dict[master.name+'AO'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} + # csv_dict[master.name+'AO']['csv_writer'].writerow(['time']+myCIMProcessor.get_dnp3_msg_AO_header()) + if master.name+'BO' not in csv_dict: + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_BO.csv', list(dnp3_msg_AO.keys())) + csv_dict[master.name+'BO'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} + # csv_dict[master.name+'BO']['csv_writer'].writerow(['time']+myCIMProcessor.get_dnp3_msg_AO_header()) + if master.name =='RTU1': + if len(dnp3_msg_AO.keys()) > 0: + # max_index = max(dnp3_msg.keys()) + values = [dnp3_msg_AO[k] for k in dnp3_msg_AO.keys()] + csv_dict[master.name+'AO']['csv_writer'].writerow(np.insert(values,0, msg_count)) + csv_dict[master.name+'AO']['csv_file'].flush() + if len(dnp3_msg_BO.keys()) > 0: + # max_index = max(dnp3_msg.keys()) + values = [dnp3_msg_BO[k] for k in dnp3_msg_BO.keys()] + csv_dict[master.name+'BO']['csv_writer'].writerow(np.insert(values,0, msg_count)) + csv_dict[master.name+'BO']['csv_file'].flush() + + + if len(dnp3_msg_AI.keys()) > 0: # max_index = max(dnp3_msg.keys()) - values = [dnp3_msg[k] for k in dnp3_msg.keys()] - csv_dict[master.name]['csv_writer'].writerow(np.insert(values,0, msg_count)) - csv_dict[master.name]['csv_file'].flush() + values = [dnp3_msg_AI[k] for k in dnp3_msg_AI.keys()] + csv_dict[master.name+'AI']['csv_writer'].writerow(np.insert(values,0, msg_count)) + csv_dict[master.name+'AI']['csv_file'].flush() else: print("no data yet") @@ -191,8 +221,8 @@ def run_master(device_ip_port_config_all, names): names.append(device_name) if 'shark' in device_name[:5]: names.append(device_name) - if 'capbank' in device_name: - names.append(device_name) + # if 'capbank' in device_name: + # names.append(device_name) print("Running "+ str(names)) time.sleep(2) From 60d40f9e2bd53d5b86bca94c899a740a824610b2 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Fri, 19 Mar 2021 14:57:11 -0600 Subject: [PATCH 58/64] Fixed header issue with CSV --- test/CIMProcessor.py | 35 +++++++++++++++++++++++------------ test/master_main.py | 2 +- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index a179795..1cdb454 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -36,6 +36,27 @@ def __init__(self, point_definitions,master): self._dnp3_msg_BO = {} self._dnp3_msg_AO_header = [] self._dnp3_msg_BO_header = [] + + cap_point1 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) + cap_point1.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" + cap_point2 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=1, op_type=None) + cap_point2.measurement_id = "_C1706031-2C1C-464C-8376-6A51FA70B470" + cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=2, op_type=None) + cap_point3.measurement_id = "_245E3924-8292-46D5-A11E-C80F7D6EE253" + self._cap_list = [cap_point1,cap_point2,cap_point3] + reg_point1 = PointValue(command_type=None, function_code=None, value=0, point_def=0, index=0, op_type=None) + reg_point1.measurement_id = "_D081C22C-D840-4303-8C95-C9151610C9A6" + self._reg_list = [reg_point1] + + for point in self._cap_list: + self._dnp3_msg_BO[point.index] = point.value + + for point in self._reg_list: + self._dnp3_msg_AO[point.index] = point.value + + for point in self._pv_points: + self._dnp3_msg_AO[point.index] = point.value + self.lock = threading.Lock() @@ -78,16 +99,6 @@ def process(self, message): # {"bankName":"s1","size":"1","bankPhases":"ABC","tankName":[""],"endNumber":[2],"endPhase":["ABC"],"rtcName":["s1"],"mRID":["_D081C22C-D840-4303-8C95-C9151610C9A6"],"monitoredPhase":["A"],"TapChanger.tculControlMode":["volt"],"highStep":[16],"lowStep":[-16],"neutralStep":[0],"normalStep":[0],"TapChanger.controlEnabled":[true],"lineDropCompensation":[true],"ltcFlag":[true],"RegulatingControl.enabled":[true],"RegulatingControl.discrete":[true],"RegulatingControl.mode":["voltage"],"step":[-8],"targetValue":[123.0000],"targetDeadband":[2.0000],"limitVoltage":[0.0000],"stepVoltageIncrement":[0.6250],"neutralU":[6927.6000],"initialDelay":[30.0000],"subsequentDelay":[2.0000],"lineDropR":[7.0000],"lineDropX":[0.0000],"reverseLineDropR":[0.0000],"reverseLineDropX":[0.0000],"ctRating":[300.0000],"ctRatio":[1500.0000],"ptRatio":[57.7300]} # ], - cap_point1 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=0, op_type=None) - cap_point1.measurement_id = "_5955BE75-5EE5-477A-936F-65EDE5E3B831" - cap_point2 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=1, op_type=None) - cap_point2.measurement_id = "_C1706031-2C1C-464C-8376-6A51FA70B470" - cap_point3 = PointValue(command_type=None, function_code=None, value=1, point_def=0, index=2, op_type=None) - cap_point3.measurement_id = "_245E3924-8292-46D5-A11E-C80F7D6EE253" - cap_list = [cap_point1,cap_point2,cap_point3] - reg_point1 = PointValue(command_type=None, function_code=None, value=100, point_def=0, index=0, op_type=None) - reg_point1.measurement_id = "_D081C22C-D840-4303-8C95-C9151610C9A6" - reg_list = [reg_point1] # "_5955BE75-5EE5-477A-936F-65EDE5E3B831" # master = masters[0] @@ -106,7 +117,7 @@ def process(self, message): # print(master) self._dnp3_msg_AO = {} self._dnp3_msg_BO = {} - for point in reg_list: + for point in self._reg_list: if command.get("object") == point.measurement_id : self._dnp3_msg_AO[point.index] = point.value if command.get("object") == point.measurement_id and point.value != command.get("value"): @@ -118,7 +129,7 @@ def process(self, message): point.index, command_callback) - for point in cap_list: + for point in self._cap_list: # Capbank if command.get("object") == point.measurement_id : self._dnp3_msg_BO[point.index] = point.value diff --git a/test/master_main.py b/test/master_main.py index b707296..c6c7a64 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -149,7 +149,7 @@ def run_master(device_ip_port_config_all, names): csv_dict[master.name+'AO'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} # csv_dict[master.name+'AO']['csv_writer'].writerow(['time']+myCIMProcessor.get_dnp3_msg_AO_header()) if master.name+'BO' not in csv_dict: - rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_BO.csv', list(dnp3_msg_AO.keys())) + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_BO.csv', list(dnp3_msg_BO.keys())) csv_dict[master.name+'BO'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} # csv_dict[master.name+'BO']['csv_writer'].writerow(['time']+myCIMProcessor.get_dnp3_msg_AO_header()) if master.name =='RTU1': From 6a6f556dd5372d58a917e1527a9490351f920765 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Mon, 22 Mar 2021 14:58:25 -0600 Subject: [PATCH 59/64] added irradiance --- dnp3/service/dnp3/master.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index f8bedb4..861ea14 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -342,6 +342,10 @@ def get_dnp3_msg_BI_header(self): return self._dnp3_msg_BI_header def update_cim_msg_analog_multi_index(self, CIM_msg, index, value, conversion, model): + # model_line_dict['irradiance'] + if ['CIM name'] == 'irradiance': + CIM_msg['irradiance'] = value + return CIM_phase = conversion[index]['CIM phase'] CIM_units = conversion[index]['CIM units'] CIM_attribute = conversion[index]['CIM attribute'] From 1ab70b995a72427b8163b056befb9c0856598cf9 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 21 Apr 2021 11:06:05 -0600 Subject: [PATCH 60/64] Commented out print statements --- dnp3/service/dnp3/master.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/dnp3/service/dnp3/master.py b/dnp3/service/dnp3/master.py index 861ea14..36a7aca 100644 --- a/dnp3/service/dnp3/master.py +++ b/dnp3/service/dnp3/master.py @@ -343,8 +343,9 @@ def get_dnp3_msg_BI_header(self): def update_cim_msg_analog_multi_index(self, CIM_msg, index, value, conversion, model): # model_line_dict['irradiance'] - if ['CIM name'] == 'irradiance': + if conversion[index]['CIM name'] == 'irradiance': CIM_msg['irradiance'] = value + print('irradiance', value) return CIM_phase = conversion[index]['CIM phase'] CIM_units = conversion[index]['CIM units'] @@ -489,13 +490,13 @@ def Process(self, info, values): ## TODO check for each type seperate binary vs analog ## Analog check with self.lock: - print(type(values)) - print(visitor) + # print(type(values)) + # print(visitor) if type(values) == opendnp3.ICollectionIndexedAnalog: - print(dir(values)) + # print(dir(values)) # values.ForeachItem() for index, value in visitor.index_and_value: - print("Jeff SOE IndexedAnalog", index, value, isinstance(value, numbers.Number)) + # print("Jeff SOE IndexedAnalog", index, value, isinstance(value, numbers.Number)) # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) if not self._dnp3_msg_AI_header: @@ -505,12 +506,14 @@ def Process(self, info, values): else: self._dnp3_msg_AI_header = [v['CIM name']+'_'+v['CIM units'] for k, v in conversion['Analog input'].items()] if 'RTU' in self._device: - print('Jeff RTU') + # print('Jeff RTU') self._dnp3_msg_AI[index]=value if index in conversion_name_index_dict: # _log.debug("Conversion for " + str(index)) model = model_line_dict[conversion_name_index_dict[index]['CIM name']] + if int(index) == 129: + print(129, model, value) self.update_cim_msg_analog_multi_index(self._cim_msg,index,value,conversion_name_index_dict,model) else: _log.debug("No conversion for " + str(index)) @@ -545,7 +548,7 @@ def Process(self, info, values): ## '/topic/goss.gridappsd.fim.input' elif type(values) == opendnp3.ICollectionIndexedBinary: if 'RTU' in self._device and 'Binary input' in conversion: - print('Jeff Binary RTU') + # print('Jeff Binary RTU') for index, value in visitor.index_and_value: self._dnp3_msg_BI[index]=value conversion_name_index_dict = {v['index']: v for k, v in conversion['Binary input'].items()} @@ -558,13 +561,14 @@ def Process(self, info, values): _log.debug("No conversion for " + str(index)) else: for index, value in visitor.index_and_value: - print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) + # print("Jeff SOE Binary", index, value, isinstance(value, numbers.Number)) # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) self.update_cim_msg_binary(self._cim_msg, str(float(index)), value, conversion, model) # Untested might work else: for index, value in visitor.index_and_value: - print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) + pass + # print("Jeff SOE Other", index, value, isinstance(value, numbers.Number)) # log_string = 'SOEHandlerSOEHandler.Process {0}\theaderIndex={1}\tdata_type={2}\tindex={3}\tvalue={4}' # _log.debug(log_string.format(info.gv, info.headerIndex, type(values).__name__, index, value)) From 213343835698d410bc9473704666589caad480f6 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 21 Apr 2021 11:10:01 -0600 Subject: [PATCH 61/64] Added logging. Sleep between Capabnk commands to fix seg fault.Initialized dictionaries for logging at the begining. Do not check capbank value on sending. Need to add handshake. --- test/CIMProcessor.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/test/CIMProcessor.py b/test/CIMProcessor.py index 1cdb454..87eedf2 100644 --- a/test/CIMProcessor.py +++ b/test/CIMProcessor.py @@ -5,6 +5,10 @@ import yaml from dnp3.points import PointValue import threading +import time +import logging +_log = logging.getLogger(__name__) + def collection_callback(result=None): """ @@ -56,6 +60,8 @@ def __init__(self, point_definitions,master): for point in self._pv_points: self._dnp3_msg_AO[point.index] = point.value + + print(self._dnp3_msg_AO) self.lock = threading.Lock() @@ -110,12 +116,14 @@ def process(self, message): print("input Jeff") control_values = json_msg["input"]["message"]["forward_differences"] print(control_values) + _log.info("control_values ", control_values) with self.lock: for command in control_values: + # time.sleep(.5) print("command", command) # master = self.master_dict[command["object"]] # print(master) - self._dnp3_msg_AO = {} + # self._dnp3_msg_AO = {} self._dnp3_msg_BO = {} for point in self._reg_list: if command.get("object") == point.measurement_id : @@ -130,11 +138,12 @@ def process(self, message): command_callback) for point in self._cap_list: + time.sleep(.02) # Capbank if command.get("object") == point.measurement_id : self._dnp3_msg_BO[point.index] = point.value if command.get("object") == point.measurement_id and point.value != command.get("value"): - open_cmd = command.get("value") == 0 + open_cmd = command.get("value") != 0 if open_cmd: # Open master.send_select_and_operate_command(opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON), @@ -186,13 +195,14 @@ def process(self, message): pv_points=[pv_point_tmp1,pv_point_tmp2] # PV points for point in self._pv_points: - if command.get("object") == point.measurement_id and point.value != command.get("value"): + if command.get("object") == point.measurement_id : # and point.value != command.get("value"): if command.get("attribute") == point.attribute: temp_index = point.index - point.value =float(command.get("value")) + # point.value =float(command.get("value")) point.value = int(command.get("value")) self._dnp3_msg_AO[temp_index] = point.value print("PV ",point.index, point.value, point.attribute) + _log.info("PV ",point.index, point.value, point.attribute) master.send_direct_operate_command(opendnp3.AnalogOutputInt32(point.value), # master.send_direct_operate_command(opendnp3.AnalogOutputFloat32(point.value), # master.send_direct_operate_command(opendnp3.AnalogOutputDouble64(point.value), From c6d483cdb03699e330b596aa15329dc9073de73b Mon Sep 17 00:00:00 2001 From: jsimpson Date: Wed, 21 Apr 2021 11:11:26 -0600 Subject: [PATCH 62/64] fixed logging and sleep --- test/master_main.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/test/master_main.py b/test/master_main.py index c6c7a64..91cfd10 100644 --- a/test/master_main.py +++ b/test/master_main.py @@ -123,14 +123,16 @@ def run_master(device_ip_port_config_all, names): # master.fast_scan_all.Demand() msg_count=0 csv_dict = {} - cim_full_msg = {'simulation_id': 1234, 'timestamp': str(int(time.time())), 'message':{'measurements':{}}} + cim_full_msg = {'simulation_id': 1234, 'timestamp': str(int(time.time())), 'irradiance':0.0, 'message':{'measurements':{}}} + starttime = time.time() while True: + current_time = time.time() # cim_full_msg = {'simulation_id': 1234, 'timestamp': 0, 'message':{'measurements':{}}} for master in masters: print("getting CIM from master "+master.name) cim_msg = master.soe_handler.get_msg() dnp3_msg_AI = master.soe_handler.get_dnp3_msg_AI() - dnp3_msg_AO = master.soe_handler.get_dnp3_msg_BI() + dnp3_msg_BI = master.soe_handler.get_dnp3_msg_BI() if master.name =='RTU1': dnp3_msg_AO = myCIMProcessor.get_dnp3_msg_AO() dnp3_msg_BO = myCIMProcessor.get_dnp3_msg_BO() @@ -140,7 +142,7 @@ def run_master(device_ip_port_config_all, names): csv_dict[master.name+'AI'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} csv_dict[master.name+'AI']['csv_writer'].writerow(['time']+master.soe_handler.get_dnp3_msg_AI_header()) if master.name+'BI' not in csv_dict: - rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_BI.csv', list(dnp3_msg_AI.keys())) + rtu_7_csvfile, rtu_7_writer = build_csv_writers('.', master.name+'_BI.csv', list(dnp3_msg_BI.keys())) csv_dict[master.name+'BI'] = {'csv_file':rtu_7_csvfile, 'csv_writer':rtu_7_writer} # csv_dict[master.name+'BI']['csv_writer'].writerow(['time']+master.soe_handler.get_dnp3_msg_BI_header()) if master.name =='RTU1': @@ -154,21 +156,22 @@ def run_master(device_ip_port_config_all, names): # csv_dict[master.name+'BO']['csv_writer'].writerow(['time']+myCIMProcessor.get_dnp3_msg_AO_header()) if master.name =='RTU1': if len(dnp3_msg_AO.keys()) > 0: - # max_index = max(dnp3_msg.keys()) values = [dnp3_msg_AO[k] for k in dnp3_msg_AO.keys()] - csv_dict[master.name+'AO']['csv_writer'].writerow(np.insert(values,0, msg_count)) + csv_dict[master.name+'AO']['csv_writer'].writerow(np.insert(values,0, current_time)) csv_dict[master.name+'AO']['csv_file'].flush() if len(dnp3_msg_BO.keys()) > 0: - # max_index = max(dnp3_msg.keys()) values = [dnp3_msg_BO[k] for k in dnp3_msg_BO.keys()] - csv_dict[master.name+'BO']['csv_writer'].writerow(np.insert(values,0, msg_count)) + csv_dict[master.name+'BO']['csv_writer'].writerow(np.insert(values,0, current_time)) csv_dict[master.name+'BO']['csv_file'].flush() - + if master.name =='RTU1': + if len(dnp3_msg_BI.keys()) > 0: + values = [int(dnp3_msg_BI[k]) for k in dnp3_msg_BI.keys()] + csv_dict[master.name+'BI']['csv_writer'].writerow(np.insert(values, 0, current_time)) + csv_dict[master.name+'BI']['csv_file'].flush() if len(dnp3_msg_AI.keys()) > 0: - # max_index = max(dnp3_msg.keys()) values = [dnp3_msg_AI[k] for k in dnp3_msg_AI.keys()] - csv_dict[master.name+'AI']['csv_writer'].writerow(np.insert(values,0, msg_count)) + csv_dict[master.name+'AI']['csv_writer'].writerow(np.insert(values,0, current_time)) csv_dict[master.name+'AI']['csv_file'].flush() else: print("no data yet") @@ -177,12 +180,18 @@ def run_master(device_ip_port_config_all, names): # message['message']['measurements'] cim_full_msg['message']['measurements'].update(cim_msg) cim_full_msg['timestamp'] = str(int(time.time())) - gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_full_msg)) - msg_count+=1 + + with open('meas_map.json', 'w') as outfile: + json.dump(cim_full_msg, outfile, indent=2) + gapps.send('/topic/goss.gridappsd.fim.input.'+str(1234), json.dumps(cim_full_msg)) + msg_count+=1 + print("message count "+ str(msg_count)) + _log.info("message count "+ str(msg_count)) - print(master.name+" " +str(cim_full_msg)[:100]) - _log.info(cim_full_msg) - time.sleep(1) + print(master.name+" " +str(cim_full_msg)[:100]) + _log.info(cim_full_msg) + # time.sleep(2) + time.sleep(60.0 - ((time.time() - starttime) % 60.0)) print('\nStopping') @@ -225,7 +234,7 @@ def run_master(device_ip_port_config_all, names): # names.append(device_name) print("Running "+ str(names)) - time.sleep(2) + time.sleep(1) device_ip_port_dict = device_ip_port_config_all[names[0]] print(device_ip_port_dict) From 97bc765fac4557695aafd65e37f1bc9088ff63c3 Mon Sep 17 00:00:00 2001 From: jsimpson Date: Thu, 10 Jun 2021 13:49:13 -0600 Subject: [PATCH 63/64] Added read the docs. Updated to add master_dict. Fixed input checking. Merge branch 'new_oes_branch' of github.com:GRIDAPPSD/gridappsd-dnp3 into develop # Conflicts: # .gitignore # dnp3/service/dnp3/cim_to_dnp3.py # dnp3/service/dnp3/outstation.py # dnp3/service/new_start_service.py --- docs/source/_static/ConversionDictionary.png | Bin 0 -> 89571 bytes docs/source/_static/MessageFlow.png | Bin 0 -> 84586 bytes docs/source/_static/ModelDictionary.png | Bin 0 -> 48199 bytes docs/source/_static/SimpleClass.png | Bin 0 -> 49074 bytes docs/source/index.rst | 5 + docs/source/master_service.rst | 338 +++++++++++++++++++ 6 files changed, 343 insertions(+) create mode 100644 docs/source/_static/ConversionDictionary.png create mode 100644 docs/source/_static/MessageFlow.png create mode 100644 docs/source/_static/ModelDictionary.png create mode 100644 docs/source/_static/SimpleClass.png create mode 100644 docs/source/master_service.rst diff --git a/docs/source/_static/ConversionDictionary.png b/docs/source/_static/ConversionDictionary.png new file mode 100644 index 0000000000000000000000000000000000000000..def98fb614faf6ae14202c1129f3b798ee7e71fb GIT binary patch literal 89571 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
(~rX0V2!e>SDO~!P+gE2{|k-$8glHZhSzeFHw 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 FzW`~+as&VX literal 0 HcmV?d00001 diff --git a/docs/source/_static/ModelDictionary.png b/docs/source/_static/ModelDictionary.png new file mode 100644 index 0000000000000000000000000000000000000000..da9b535cffa78deb407bda40ee309c53fb3460bd GIT binary patch literal 48199 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

C}mq*2GXxw7A2zlBFmQT9is-M4#cV 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�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 literal 0 HcmV?d00001 diff --git a/docs/source/index.rst b/docs/source/index.rst index 81eb5e3..851979b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -139,6 +139,11 @@ This received CIM message was processed and the below plot shows the state of th |switch1| +Masters +------- + +.. include:: master_service.rst + References ~~~~~~~~~~ diff --git a/docs/source/master_service.rst b/docs/source/master_service.rst new file mode 100644 index 0000000..645faab --- /dev/null +++ b/docs/source/master_service.rst @@ -0,0 +1,338 @@ + +GridAPPS-D DNP3 master is an application service to integrate GridAPPS-D + +DNP3 processing for Use Case 3 + +This paper covers the work documentation and work done for connecting to DNP3 outstation devices and converting their signals to CIM messages for use with an application built for the GridAPPS-D platform. *Mention OpenDNP3* + +Current Issues TODO-WIP +^^^^^^^^^^^^^^^^^^^^^^^ + +#. Merge the conversion dictionaries to one conversion dictionary. +#. Move script and function for master_main script to a class. +#. Convert to run as a service. + + #. Code interface to provide config json files + #. Viz to provide config json files. + #. Ports and IP address will have to be visible. + +Setup +^^^^^ + +There are 2 steps for getting started with creating the conversion files for DNP3 processing. This can take some time and should be considered and iterative process. +1. Get input help from domain experts about the lab setup for the Data Manager (SEL RTAC) and equipment that into the DNP xlsx files. There is no script template or scheme for these files at the moment. +2. Preprocessing convert the feeder model to CIM and load in the blazegraph DB, then use GridAPPS-D to create the model_dict.json + +Convert files with build_conversion_dicts.py or the Jupyter notebook. This will build the json dictionary files for converting to CIM messages. + +File descriptions + ++-------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| File | Description | ++=================================================+=====================================================================================================================================================================================================================================+ +| DNP3_Dict_Feb23(3).xlsx- (DNP3-RTAC-list.xlsx) | Excel file with a sheet for each RTU that has the OpenDSS load name, phase and DNP3 Analog Input index. RTU1 has the LTC, PV names and DNP3 index as Analog Output index. RTU1 also has Binary Input and Output for the Capbanks. | ++-------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| DNP3List.xlsx | Excel file with a sheet for each piece of equipment DNP 3 details. There are fields to match with CIM values such as magnitude, angle, or position | ++-------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| model_dict.json | The model_dict.json which has the element names and CIM MRIDS. This is generated from the CIM feeder model. | ++-------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| device_ip_port_config_all.json | The IP address, port number, and DNP3 address for each piece of equipment and RTU. | ++-------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + +The conversion dictionary files are + ++------------------------------+-----------------------------------------------------------------------------------+ +| Files | Description | ++==============================+===================================================================================+ +| conversion_dict_eq.json | Dictionary of DNP3 device to DNP3 data type input/output to DNP3 index to object | ++------------------------------+-----------------------------------------------------------------------------------+ +| conversion_dict_master.json | Dictionary of DNP3 device to DNP3 data type input/output to DNP3 index to object | ++------------------------------+-----------------------------------------------------------------------------------+ +| model_line_dict_master.json | Dictionary of OpenDSS names to CIM type to Phase to MRID | ++------------------------------+-----------------------------------------------------------------------------------+ + + +.. figure:: _static/ConversionDictionary.png + :align: left + :alt: ConversionDictionary + :figclass: align-left + +Example of the RTU7 entry in conversion_dict_master.json: + +.. code-block:: JSON + + { + "key": "value" + } + +.. code-block:: JSON + :caption: Conversion Dict Example + + { + "RTU_7": { + "Analog input": { + "0": { + "DSS to OdeepC Meter Point": "P_Tran_xf_701_1095507_5022.1", + "Measurement": "Real Power", + "DSS Marker": 11.0, + "RTAC RTU Address": 1.0, + "Analog In Index": 0, + "RTU port": 20007, + "RTAC IP": "10.79.91.42", + "Multiplier": "", + "index": 0, + "CIM phase": "A", + "CIM attribute": "magnitude", + "CIM units": "VA", + "CIM name": "tran_xf_701_1095507_5022" + } + } + } + } + +Example of an entry in model_line_dict_master.json + +.. figure:: _static/ModelDictionary.png + :align: left + :alt: ModelDictionary + :figclass: align-left + +.. code-block:: JSON + :caption: Model Dictionary Example + + { + "tran_xf_701_1095511_5025": { + "VA": { + "A": { + "mrid": "_262c1051-d1f3-4210-8464-7fc184c65290", + "type": "angle" + } + }, + "PNV": { + "A": { + "mrid": "_b546b42e-43b6-4efc-87eb-d3e215c86957", + "type": "angle" + } + } + } + } + + +Running +^^^^^^^ + +Start the master_main.py with the argument for which RTU or piece of equipment to connect to. For example ‘RTU1’ or ‘*’ for everything. The inputs at the moment are ‘RTU1’, ‘RTU2’, ‘RTU3’, ‘RTU4’, ‘RTU5’, ‘RTU6’, ‘shark 1’ and ‘shark 2’. The master_main.py will start one or more DNP3 masters and run until you kill the process. It will also save a csv file for every input. + +.. code-block:: BASH + :caption: master_main example + + python master_main.py ‘*’ + +Classes and scripts +^^^^^^^^^^^^^^^^^^^ + +build_conversion_dicts.py +build_RTAC function +Description: The build_conversion_dicts.py builds the conversion dictionaries used to convert DNP3 message to CIM messages. + ++--------------------------+--------------------------------------------------------------------------------------------------------------+ +| Parameter | Description | ++==========================+==============================================================================================================+ +| DNP3_Dict_Feb23(3).xlsx | Excel file with a sheet for each RTU. | ++--------------------------+--------------------------------------------------------------------------------------------------------------+ +| DNP3List.xlsx | Excel file with a sheet for each piece of equipment DNP 3 details | ++--------------------------+--------------------------------------------------------------------------------------------------------------+ +| model_dict.json | The model_dict.json which has the element names and CIM MRIDS. This is generated from the CIM feeder model. | ++--------------------------+--------------------------------------------------------------------------------------------------------------+ + +Master class in master.py +Class to connect to DNP3 outstations. Actual processing of messages is done by a SOEHandler. + +Constructor +Description: +Initialize connection to DNP3 outstations + ++-------------+-------------------------------------------------+ +| Parameters | Description | ++=============+=================================================+ +| HOST | Host IP address | ++-------------+-------------------------------------------------+ +| LOCAL | DNP3 local IP address | ++-------------+-------------------------------------------------+ +| PORT | DNP3 Port | ++-------------+-------------------------------------------------+ +| DNP3_ADDR | DNP3 Address | ++-------------+-------------------------------------------------+ +| LocalAddr | Local Addr | ++-------------+-------------------------------------------------+ +| soe_handler | SOE handler class that handles processes DNP3 | ++-------------+-------------------------------------------------+ + + +SOEHandler class in master.py +The SOEHandler class handles DNP3 values received from the outstation. +It is an interface for SequenceOfEvents (SOE) callbacks from the Master stack to the application layer. Note add link. + +Constructor +Description: Class that handles updates to DNP3 values + ++-------------+-------------------+ +| Parameters | Description | ++=============+===================+ +| name | name | ++-------------+-------------------+ +| device | DNP3 device name | ++-------------+-------------------+ +| dnp3_to_cim | CIMMapping class | ++-------------+-------------------+ + +process method +Description: +Use the dnp3_to_cim to maps to create the CIM message for DNP3 values +Loop through values + ++-------------+-----------------------------------------------------------------------------------------+ +| Parameters | Description | ++=============+=========================================================================================+ +| info | | ++-------------+-----------------------------------------------------------------------------------------+ +| values | A collection of values received from the Outstation (various data types are possible). | ++-------------+-----------------------------------------------------------------------------------------+ + +Input Classes + ++---------------+-----------------+--------+------------+-----------------------------------------------------------------------------+ +| Type | OpenDNP3 Class | Group | Variation | Link | ++===============+=================+========+============+=============================================================================+ +| Cap | opendnp3.Binary | 1* | 2* | https://dnp3.github.io/docs/cpp/3.0.0/db/dcf/classopendnp3_1_1_binary.html | ++---------------+-----------------+--------+------------+-----------------------------------------------------------------------------+ +| PV | opendnp3.Analog | 30* | 1* | https://dnp3.github.io/docs/cpp/3.0.0/dc/dd8/classopendnp3_1_1_analog.html | ++---------------+-----------------+--------+------------+-----------------------------------------------------------------------------+ +| Load Power | opendnp3.Analog | 30* | 1* | https://dnp3.github.io/docs/cpp/3.0.0/dc/dd8/classopendnp3_1_1_analog.html | ++---------------+-----------------+--------+------------+-----------------------------------------------------------------------------+ +| Load Voltage | opendnp3.Analog | 30* | 1* | https://dnp3.github.io/docs/cpp/3.0.0/dc/dd8/classopendnp3_1_1_analog.html | ++---------------+-----------------+--------+------------+-----------------------------------------------------------------------------+ +| Shark | opendnp3.Analog | 30* | 1* | https://dnp3.github.io/docs/cpp/3.0.0/dc/dd8/classopendnp3_1_1_analog.html | ++---------------+-----------------+--------+------------+-----------------------------------------------------------------------------+ +| Reg | opendnp3.Analog | 30* | 1* | https://dnp3.github.io/docs/cpp/3.0.0/dc/dd8/classopendnp3_1_1_analog.html | ++---------------+-----------------+--------+------------+-----------------------------------------------------------------------------+ + + +CIMMapping class in dnp3_to_cim.py +Holds dictionaries for mapping + +Constructor +Description: +Set dictionary values. + ++------------------+-----------------------+ +| Parameters | Description | ++==================+=======================+ +| conversion_dict | Conversion_dict file | ++------------------+-----------------------+ +| model_line_dict | Model_line_dict file | ++------------------+-----------------------+ + +master_main.py script +main function +Description: +Read list of device names from command line and call run_masters. + +run_masters function +Description: + +#. Create masters using the name in the names list to get the IP, PORT, DNP3 addr etc... in device_ip_port_config_all.json. Also pass the dnp3_to_cim mapping dictionaries for conversion_dict_master.json and model_line_dict_master.json to each master. +#. Initialize cim_full_msg dictionary with the basics of a CIM message +#. While loop for getting CIM messages + + #. For each master get the converted CIM messages and update the cim_full_msg. + #. Send the cim_full_msg to the application. + #. Write data to cvs file for each master. + #. Sleeps for 60 seconds + ++----------------------------+-------------------------------+ +| Parameters | Description | ++============================+===============================+ +| device_ip_port_config_all | CIM message from application | ++----------------------------+-------------------------------+ +| names | List of DNP3 Master names | ++----------------------------+-------------------------------+ + + +on_message function +Description: +Use the CIMProcessor to process the command from the application + ++-------------+-------------------------------+ +| Parameters | Description | ++=============+===============================+ +| message | CIM message from application | ++-------------+-------------------------------+ + + +CIMProcessor class in CIMProcessor.py +Description: +CIMProcessor handles commands from the application and sends the values to the outstation through the master. +At the moment there are regulator, capacitor and a pv list of point definitions. +Constructor + ++---------------------+----------------------------------------------------------------------------------------------------------------------+ +| Parameters | Description | ++=====================+======================================================================================================================+ +| point_definitions | Point definition which is just the PV point definitions | ++---------------------+----------------------------------------------------------------------------------------------------------------------+ +| master | The DNP3 master used to send commands. For Use Case 3 there was only one master RTU1 used to send all the commands. | ++---------------------+----------------------------------------------------------------------------------------------------------------------+ + + +process method +Description: + +.. code-block:: + :caption: Pseudo code for processing + + Get the control_values from the messages. + Pseudo code for processing control input commands from message: + For command in control_values: + For point in point_list: + If command.get("object") == point.measurement_id: + point.value = command.get("value") + master.send_direct_operate_command(point.value, + temp_index, + command_callback) + + ++-------------+----------------------------------------------------------------------------------------------------------------------+ +| Parameters | Description | ++=============+======================================================================================================================+ +| message | Message from GOSS message bus | ++-------------+----------------------------------------------------------------------------------------------------------------------+ +| master | The DNP3 master used to send commands. For Use Case 3 there was only one master RTU1 used to send all the commands. | ++-------------+----------------------------------------------------------------------------------------------------------------------+ + +Output Classes + ++-------+------------------------------------------------------------------------------------------------------+--------+------------+-------------------------------------------------------------------------------------------------+ +| Type | OpenDNP3 Class | Group | Variation | Link | ++=======+======================================================================================================+========+============+=================================================================================================+ +| Cap | opendnp3.ControlRelayOutputBlock(opendnp3.ControlCode.LATCH_ON or opendnp3.ControlCode.LATCH_OFF) | 12* | 1* | https://dnp3.github.io/docs/cpp/3.0.0/d4/d3c/classopendnp3_1_1_control_relay_output_block.html | ++-------+------------------------------------------------------------------------------------------------------+--------+------------+-------------------------------------------------------------------------------------------------+ +| PV | opendnp3.AnalogOutputInt32() | 41 | 1 | https://dnp3.github.io/docs/cpp/3.0.0/de/d53/classopendnp3_1_1_analog_output_int32.html | ++-------+------------------------------------------------------------------------------------------------------+--------+------------+-------------------------------------------------------------------------------------------------+ +| Reg | opendnp3.AnalogOutputInt32() | 41 | 1 | https://dnp3.github.io/docs/cpp/3.0.0/de/d53/classopendnp3_1_1_analog_output_int32.html | ++-------+------------------------------------------------------------------------------------------------------+--------+------------+-------------------------------------------------------------------------------------------------+ + +Class Diagram and Message Flow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. figure:: _static/MessageFlow.png + :align: left + :alt: MessageFlow + :figclass: align-left + +.. figure:: _static/SimpleClass.png + :align: left + :alt: SimpleClass + :figclass: align-left + From 545a535f59c66a4c43db47c5384fe82db28eb41f Mon Sep 17 00:00:00 2001 From: jsimpson Date: Thu, 10 Jun 2021 16:50:45 -0600 Subject: [PATCH 64/64] Updated Intro --- docs/source/index.rst | 4 ++-- docs/source/master_service.rst | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 851979b..3259c46 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -139,8 +139,8 @@ This received CIM message was processed and the below plot shows the state of th |switch1| -Masters -------- +Master Service +-------------- .. include:: master_service.rst diff --git a/docs/source/master_service.rst b/docs/source/master_service.rst index 645faab..084d5c5 100644 --- a/docs/source/master_service.rst +++ b/docs/source/master_service.rst @@ -1,9 +1,19 @@ -GridAPPS-D DNP3 master is an application service to integrate GridAPPS-D +The GridAPPS-D DNP3 master python scripts is the starting point for a service to integrate the GridAPPS-D message bus and application with DNP3 outstations points. +The master_main script creates masters to DNP3 outstation devices and converts their DNP3 point values to CIM messages for use with an application built for the GridAPPS-D platform. -DNP3 processing for Use Case 3 +#. Work to be done +#. The setup steps +#. Running the script +#. The Classes and scripts discriptions -This paper covers the work documentation and work done for connecting to DNP3 outstation devices and converting their signals to CIM messages for use with an application built for the GridAPPS-D platform. *Mention OpenDNP3* +This paper covers the work documentation and work done for connecting to DNP3 outstation devices and converting their signals to CIM messages for use with an application built for the GridAPPS-D platform. + +OpenDNP3 docs +^^^^^^^^^^^^^ + +The website for OpenDNP3 is at https://dnp3.github.io/ +The API documentation is at https://dnp3.github.io/docs/cpp/3.0.0/index.html Current Issues TODO-WIP ^^^^^^^^^^^^^^^^^^^^^^^ @@ -148,6 +158,7 @@ Description: The build_conversion_dicts.py builds the conversion dictionaries us Master class in master.py Class to connect to DNP3 outstations. Actual processing of messages is done by a SOEHandler. +https://dnp3.github.io/docs/guide/3.0.0/api/masters/#isoehandler Constructor Description: