gridappsd-field-bus 2023.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.1
2
+ Name: gridappsd-field-bus
3
+ Version: 2023.5
4
+ Summary: GridAPPS-D Field Bus Implementation
5
+ Home-page: https://gridappsd.readthedocs.io
6
+ License: BSD-3-Clause
7
+ Keywords: gridappsd,grid,activmq,powergrid,simulation,library
8
+ Author: C. Allwardt
9
+ Author-email: 3979063+craig8@users.noreply.github.com
10
+ Requires-Python: >=3.7.9,<4.0
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Dist: cim-graph (>=2023.5.1a3,<2024.0.0)
18
+ Requires-Dist: gridappsd-python (>=2023.5,<2024.0)
19
+ Project-URL: Repository, https://github.com/GRIDAPPSD/gridappsd-python
20
+ Description-Content-Type: text/markdown
21
+
22
+
File without changes
File without changes
@@ -0,0 +1,9 @@
1
+ from typing import List
2
+
3
+ from gridappsd.field_interface.context import LocalContext
4
+ from gridappsd.field_interface.interfaces import MessageBusDefinition
5
+
6
+ __all__: List[str] = [
7
+ "LocalContext",
8
+ "MessageBusDefinition"
9
+ ]
@@ -0,0 +1,15 @@
1
+ from typing import List
2
+
3
+ from gridappsd.field_interface.agents.agents import (
4
+ FeederAgent,
5
+ DistributedAgent,
6
+ CoordinatingAgent,
7
+ SwitchAreaAgent,
8
+ SecondaryAreaAgent
9
+ )
10
+
11
+ __all__: List[str] = [
12
+ "FeederAgent",
13
+ "DistributedAgent",
14
+ "CoordinatingAgent"
15
+ ]
@@ -0,0 +1,353 @@
1
+ import dataclasses
2
+ import importlib
3
+ import json
4
+ import logging
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import Dict
8
+
9
+ from cimgraph.loaders import ConnectionParameters, gridappsd
10
+ from cimgraph.loaders.gridappsd import GridappsdConnection
11
+ from cimgraph.models import DistributedModel, SecondaryArea, SwitchArea
12
+
13
+ import gridappsd.topics as t
14
+ from gridappsd.field_interface.context import LocalContext
15
+ from gridappsd.field_interface.gridappsd_field_bus import GridAPPSDMessageBus
16
+ from gridappsd.field_interface.interfaces import (FieldMessageBus,
17
+ MessageBusDefinition)
18
+
19
+ cim = None
20
+ sparql = None
21
+
22
+ _log = logging.getLogger(__name__)
23
+
24
+
25
+ def set_cim_profile(cim_profile):
26
+ global cim
27
+ cim = importlib.import_module('cimgraph.data_profile.' + cim_profile)
28
+ gridappsd.set_cim_profile(cim_profile)
29
+
30
+
31
+ @dataclass
32
+ class AgentRegistrationDetails:
33
+ agent_id: str
34
+ app_id: str
35
+ description: str
36
+ upstream_message_bus_id: FieldMessageBus.id
37
+ downstream_message_bus_id: FieldMessageBus.id
38
+
39
+
40
+ class DistributedAgent:
41
+
42
+ def __init__(self,
43
+ upstream_message_bus_def: MessageBusDefinition,
44
+ downstream_message_bus_def: MessageBusDefinition,
45
+ agent_config,
46
+ agent_area_dict=None,
47
+ simulation_id=None,
48
+ cim_profile: str = None):
49
+ """
50
+ Creates a DistributedAgent object that connects to the specified message
51
+ buses and gets context based on feeder id and area id.
52
+ """
53
+ _log.debug(f"Creating DistributedAgent: {self.__class__.__name__}")
54
+ self.upstream_message_bus = None
55
+ self.downstream_message_bus = None
56
+ self.simulation_id = simulation_id
57
+ self.context = None
58
+
59
+ #TODO: Change params and connection to local connection
60
+ self.params = ConnectionParameters()
61
+ self.connection = GridappsdConnection(self.params)
62
+
63
+ self.app_id = agent_config['app_id']
64
+ self.description = agent_config['description']
65
+ dt = datetime.now()
66
+ ts = datetime.timestamp(dt)
67
+ self.agent_id = "da_" + self.app_id + "_" + str(int(ts))
68
+ self.agent_area_dict = agent_area_dict
69
+
70
+ if upstream_message_bus_def is not None:
71
+ if upstream_message_bus_def.is_ot_bus:
72
+ self.upstream_message_bus = GridAPPSDMessageBus(
73
+ upstream_message_bus_def)
74
+ # else:
75
+ # self.upstream_message_bus = VolttronMessageBus(upstream_message_bus_def)
76
+
77
+ if downstream_message_bus_def is not None:
78
+ if downstream_message_bus_def.is_ot_bus:
79
+ self.downstream_message_bus = GridAPPSDMessageBus(
80
+ downstream_message_bus_def)
81
+ # else:
82
+ # self.downstream_message_bus = VolttronMessageBus(downstream_message_bus_def)
83
+
84
+ # self.context = ContextManager.get(self.feeder_id, self.area_id)
85
+
86
+ #if agent_dict is not None:
87
+ # self.addressable_equipments = agent_dict['addressable_equipment']
88
+ # self.unaddressable_equipments = agent_dict['unaddressable_equipment']
89
+
90
+ def connect(self):
91
+
92
+ if self.upstream_message_bus is not None:
93
+ self.upstream_message_bus.connect()
94
+ if self.downstream_message_bus is not None:
95
+ self.downstream_message_bus.connect()
96
+ if self.downstream_message_bus is None and self.upstream_message_bus is None:
97
+ raise ValueError(
98
+ "Either upstream or downstream bus must be specified!")
99
+
100
+ if self.agent_area_dict is None:
101
+ context = LocalContext.get_context_by_message_bus(
102
+ self.downstream_message_bus)
103
+ self.agent_area_dict = context['data']
104
+
105
+ self.subscribe_to_measurement()
106
+ self.subscribe_to_messages()
107
+ self.subscribe_to_requests()
108
+
109
+ if ('context_manager' not in self.app_id):
110
+ LocalContext.register_agent(self.downstream_message_bus,
111
+ self.upstream_message_bus, self)
112
+
113
+ def subscribe_to_measurement(self):
114
+ if self.simulation_id is None:
115
+ self.downstream_message_bus.subscribe(
116
+ t.field_output_topic(self.downstream_message_bus.id),
117
+ self.on_measurement)
118
+ else:
119
+ topic = t.field_output_topic(self.downstream_message_bus.id,
120
+ self.simulation_id)
121
+ _log.debug(f"subscribing to simulation output on topic {topic}")
122
+ self.downstream_message_bus.subscribe(topic,
123
+ self.on_simulation_output)
124
+
125
+ def subscribe_to_messages(self):
126
+
127
+ self.downstream_message_bus.subscribe(
128
+ t.field_message_bus_topic(self.downstream_message_bus),
129
+ self.on_downstream_message)
130
+ self.downstream_message_bus.subscribe(
131
+ t.field_message_bus_topic(self.upstream_message_bus),
132
+ self.on_upstream_message)
133
+
134
+ _log.debug(
135
+ f"Subscribing to messages on application topics: \n {t.field_message_bus_app_topic(self.downstream_message_bus.id, self.app_id)} \
136
+ \n {t.field_message_bus_app_topic(self.upstream_message_bus.id, self.app_id)}"
137
+ )
138
+ self.downstream_message_bus.subscribe(
139
+ t.field_message_bus_app_topic(self.downstream_message_bus.id,
140
+ self.app_id),
141
+ self.on_downstream_message)
142
+ self.downstream_message_bus.subscribe(
143
+ t.field_message_bus_app_topic(self.upstream_message_bus.id,
144
+ self.app_id),
145
+ self.on_upstream_message)
146
+
147
+ _log.debug(
148
+ f"Subscribing to message on agents topics: \n {t.field_message_bus_agent_topic(self.downstream_message_bus.id, self.agent_id)} \
149
+ \n {t.field_message_bus_agent_topic(self.upstream_message_bus.id, self.agent_id)}"
150
+ )
151
+ self.downstream_message_bus.subscribe(
152
+ t.field_message_bus_agent_topic(self.downstream_message_bus.id,
153
+ self.agent_id),
154
+ self.on_downstream_message)
155
+ self.downstream_message_bus.subscribe(
156
+ t.field_message_bus_agent_topic(self.upstream_message_bus.id,
157
+ self.agent_id),
158
+ self.on_upstream_message)
159
+
160
+ def subscribe_to_requests(self):
161
+
162
+ _log.debug(
163
+ f"Subscribing to requests on agents queue: \n {t.field_agent_request_queue(self.downstream_message_bus.id, self.agent_id)} \
164
+ \n {t.field_agent_request_queue(self.upstream_message_bus.id, self.agent_id)}"
165
+ )
166
+ self.downstream_message_bus.subscribe(
167
+ t.field_agent_request_queue(self.downstream_message_bus.id,
168
+ self.agent_id),
169
+ self.on_request_from_downstream)
170
+ self.downstream_message_bus.subscribe(
171
+ t.field_agent_request_queue(self.upstream_message_bus.id,
172
+ self.agent_id),
173
+ self.on_request_from_uptream)
174
+
175
+ def on_measurement(self, headers: Dict, message) -> None:
176
+ raise NotImplementedError(
177
+ f"{self.__class__.__name__} must be overriden in child class")
178
+
179
+ def on_simulation_output(self, headers, message):
180
+ self.on_measurement(headers=headers, message=message)
181
+
182
+ def on_upstream_message(self, headers: Dict, message) -> None:
183
+ raise NotImplementedError(
184
+ f"{self.__class__.__name__} must be overriden in child class")
185
+
186
+ def on_downstream_message(self, headers: Dict, message) -> None:
187
+ raise NotImplementedError(
188
+ f"{self.__class__.__name__} must be overriden in child class")
189
+
190
+ def on_request_from_uptream(self, headers: Dict, message):
191
+ self.on_request(self.upstream_message_bus, headers, message)
192
+
193
+ def on_request_from_downstream(self, headers: Dict, message):
194
+ self.on_request(self.downstream_message_bus, headers, message)
195
+
196
+ def on_request(self, message_bus, headers: Dict, message):
197
+ raise NotImplementedError(
198
+ f"{self.__class__.__name__} must be overriden in child class")
199
+
200
+ def get_registration_details(self):
201
+ details = AgentRegistrationDetails(str(self.agent_id), self.app_id,
202
+ self.description,
203
+ self.upstream_message_bus.id,
204
+ self.downstream_message_bus.id)
205
+ return dataclasses.asdict(details)
206
+
207
+
208
+ ''' TODO this has not been implemented yet, so we are commented them out for now.
209
+ # not all agent would use this
210
+ def on_control(self, control):
211
+ device_id = control.get('device')
212
+ command = control.get('command')
213
+ self.control_device(device_id, command)
214
+
215
+ def control_device(self, device_id, command):
216
+ device_topic = self.devices.get(device_id)
217
+ self.secondary_message_bus.publish(device_topic, command)'''
218
+
219
+
220
+ class FeederAgent(DistributedAgent):
221
+
222
+ def __init__(self,
223
+ upstream_message_bus_def: MessageBusDefinition,
224
+ downstream_message_bus_def: MessageBusDefinition,
225
+ agent_config: Dict,
226
+ feeder_dict=None,
227
+ simulation_id=None):
228
+ super(FeederAgent,
229
+ self).__init__(upstream_message_bus_def,
230
+ downstream_message_bus_def, agent_config,
231
+ feeder_dict, simulation_id)
232
+ self.feeder_area = None
233
+ self.downstream_message_bus_def = downstream_message_bus_def
234
+ if self.agent_area_dict is not None:
235
+ feeder = cim.Feeder(mRID=self.downstream_message_bus_def.id)
236
+ self.feeder_area = DistributedModel(connection=self.connection,
237
+ feeder=feeder,
238
+ topology=self.agent_area_dict)
239
+
240
+
241
+ def connect(self):
242
+ super().connect()
243
+ if self.feeder_area is None:
244
+ feeder = cim.Feeder(mRID=self.downstream_message_bus_def.id)
245
+ self.feeder_area = DistributedModel(connection=self.connection,
246
+ feeder=feeder,
247
+ topology=self.agent_area_dict)
248
+
249
+
250
+ class SwitchAreaAgent(DistributedAgent):
251
+
252
+ def __init__(self,
253
+ upstream_message_bus_def: MessageBusDefinition,
254
+ downstream_message_bus_def: MessageBusDefinition,
255
+ agent_config: Dict,
256
+ switch_area_dict=None,
257
+ simulation_id=None):
258
+ super().__init__(upstream_message_bus_def, downstream_message_bus_def,
259
+ agent_config, switch_area_dict, simulation_id)
260
+ self.switch_area = None
261
+ self.downstream_message_bus_def = downstream_message_bus_def
262
+ if self.agent_area_dict is not None:
263
+ self.switch_area = SwitchArea(self.downstream_message_bus_def.id,
264
+ self.connection)
265
+ self.switch_area.initialize_switch_area(self.agent_area_dict)
266
+
267
+
268
+ def connect(self):
269
+ super().connect()
270
+ if self.switch_area is None:
271
+ self.switch_area = SwitchArea(self.downstream_message_bus_def.id,
272
+ self.connection)
273
+ self.switch_area.initialize_switch_area(self.agent_area_dict)
274
+
275
+
276
+ class SecondaryAreaAgent(DistributedAgent):
277
+
278
+ def __init__(self,
279
+ upstream_message_bus_def: MessageBusDefinition,
280
+ downstream_message_bus_def: MessageBusDefinition,
281
+ agent_config: Dict,
282
+ secondary_area_dict=None,
283
+ simulation_id=None):
284
+ super().__init__(upstream_message_bus_def, downstream_message_bus_def,
285
+ agent_config, secondary_area_dict, simulation_id)
286
+ self.secondary_area = None
287
+ self.downstream_message_bus_def = downstream_message_bus_def
288
+ if self.agent_area_dict is not None:
289
+ self.secondary_area = SecondaryArea(self.downstream_message_bus_def.id,
290
+ self.connection)
291
+ self.secondary_area.initialize_secondary_area(self.agent_area_dict)
292
+
293
+
294
+ def connect(self):
295
+ super().connect()
296
+ if self.secondary_area is None:
297
+ self.secondary_area = SecondaryArea(self.downstream_message_bus_def.id,
298
+ self.connection)
299
+ self.secondary_area.initialize_secondary_area(self.agent_area_dict)
300
+
301
+
302
+ class CoordinatingAgent:
303
+ """
304
+ A CoordinatingAgent performs following functions:
305
+ 1. Spawns distributed agents
306
+ 2. Publishes compiled output to centralized OT bus
307
+ 3. Distributes centralized output to Feeder bus and distributed agents
308
+ 4. May have connected devices and control those devices
309
+
310
+ upstream, peer , downstream and broadcast
311
+ """
312
+
313
+ def __init__(self,
314
+ feeder_id,
315
+ system_message_bus_def: MessageBusDefinition,
316
+ simulation_id=None):
317
+ self.feeder_id = feeder_id
318
+ self.distributed_agents = []
319
+
320
+ self.system_message_bus = GridAPPSDMessageBus(system_message_bus_def)
321
+ self.system_message_bus.connect()
322
+
323
+ #This will change when we have multiple feeders per system
324
+ self.downstream_message_bus = self.system_message_bus
325
+
326
+ # self.context = ContextManager.getContextByFeeder(self.feeder_id)
327
+ # print(self.context)
328
+ # self.addressable_equipments = self.context['data']['addressable_equipment']
329
+ # self.unaddressable_equipments = self.context['data']['unaddressable_equipment']
330
+ # self.switch_areas = self.context['data']['switch_areas']
331
+
332
+ # self.subscribe_to_feeder_bus()
333
+
334
+ def spawn_distributed_agent(self, distributed_agent: DistributedAgent):
335
+ distributed_agent.connect()
336
+ self.distributed_agents.append(distributed_agent)
337
+
338
+
339
+ '''
340
+ def on_control(self, control):
341
+ device_id = control.get('device')
342
+ command = control.get('command')
343
+ self.control_device(device_id, command)
344
+
345
+ def publish_to_distribution_bus(self,message):
346
+ self.publish_to_downstream_bus(message)
347
+
348
+ def publish_to_distribution_bus_agent(self,agent_id, message):
349
+ self.publish_to_downstream_bus_agent(agent_id, message)
350
+
351
+ def control_device(self, device_id, command):
352
+ device_topic = self.devices.get(device_id)
353
+ self.secondary_message_bus.publish(device_topic, command)'''
@@ -0,0 +1,51 @@
1
+ from gridappsd.field_interface.interfaces import FieldMessageBus
2
+ import dataclasses
3
+ import gridappsd.topics as t
4
+ import json
5
+
6
+
7
+
8
+ class LocalContext:
9
+
10
+ @classmethod
11
+ def get_context_by_feeder(cls, downstream_message_bus: FieldMessageBus, feeder_mrid, area_id=None):
12
+
13
+ request = {'request_type' : 'get_context',
14
+ 'modelId': feeder_mrid,
15
+ 'areaId': area_id}
16
+ response = downstream_message_bus.get_response(t.context_request_queue(downstream_message_bus.id), request, timeout=10)
17
+ return response
18
+
19
+ @classmethod
20
+ def get_context_by_message_bus(cls, downstream_message_bus: FieldMessageBus):
21
+ """
22
+ return agents/devices based on downstream message bus as input
23
+
24
+ """
25
+ request = {'request_type' : 'get_context',
26
+ 'downstream_message_bus_id': downstream_message_bus.id
27
+ }
28
+ return downstream_message_bus.get_response(t.context_request_queue(downstream_message_bus.id), request)
29
+
30
+ @classmethod
31
+ def register_agent(cls, downstream_message_bus: FieldMessageBus, upstream_message_bus: FieldMessageBus, agent):
32
+ """
33
+ Sends the newly created distributed agent's info to OT bus
34
+
35
+ """
36
+ request = {'request_type' : 'register_agent',
37
+ 'agent' : agent.get_registration_details()}
38
+ downstream_message_bus.send(t.context_request_queue(downstream_message_bus.id), request)
39
+ upstream_message_bus.send(t.context_request_queue(upstream_message_bus.id), request)
40
+
41
+ @classmethod
42
+ def get_agents(cls, downstream_message_bus: FieldMessageBus):
43
+ """
44
+ Sends the newly created distributed agent's info to OT bus
45
+
46
+ """
47
+ request = {'request_type' : 'get_agents'}
48
+ return downstream_message_bus.get_response(t.context_request_queue(downstream_message_bus.id), request)
49
+
50
+ # Provide context based on router (ip trace) or PKI
51
+ # Maybe able to emulate/simulate
@@ -0,0 +1,58 @@
1
+ from gridappsd import GridAPPSD
2
+ from gridappsd.field_interface.interfaces import FieldMessageBus
3
+ from gridappsd.field_interface.interfaces import MessageBusDefinition
4
+ from typing import Any
5
+
6
+
7
+ class GridAPPSDMessageBus(FieldMessageBus):
8
+ def __init__(self, definition: MessageBusDefinition):
9
+ super(GridAPPSDMessageBus, self).__init__(definition)
10
+ self._id = definition.id
11
+
12
+ self._user = definition.conneciton_args["GRIDAPPSD_USER"]
13
+ self._password = definition.conneciton_args["GRIDAPPSD_PASSWORD"]
14
+ self._address = definition.conneciton_args["GRIDAPPSD_ADDRESS"]
15
+
16
+ self.gridappsd_obj = None
17
+
18
+ def query_devices(self) -> dict:
19
+ pass
20
+
21
+ def is_connected(self) -> bool:
22
+ """
23
+ Is this object connected to the message bus
24
+ """
25
+ pass
26
+
27
+ def connect(self):
28
+ """
29
+ Connect to the concrete message bus that implements this interface.
30
+ """
31
+ self.gridappsd_obj = GridAPPSD()
32
+
33
+ def subscribe(self, topic, callback):
34
+ if self.gridappsd_obj is not None:
35
+ self.gridappsd_obj.subscribe(topic, callback)
36
+
37
+ def unsubscribe(self, topic):
38
+ pass
39
+
40
+ def send(self, topic: str, message: Any):
41
+ """
42
+ Publish device specific data to the concrete message bus.
43
+ """
44
+ if self.gridappsd_obj is not None:
45
+ self.gridappsd_obj.send(topic, message)
46
+
47
+ def get_response(self, topic, message, timeout=5):
48
+ """
49
+ Sends a message on a specific concrete queue, waits and returns the response
50
+ """
51
+ if self.gridappsd_obj is not None:
52
+ return self.gridappsd_obj.get_response(topic, message, timeout)
53
+
54
+ def disconnect(self):
55
+ """
56
+ Disconnect the device from the concrete message bus.
57
+ """
58
+ pass
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ import gridappsd.topics as t
7
+ import logging
8
+ from os import PathLike
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Union
11
+
12
+
13
+ import yaml
14
+
15
+
16
+ _log = logging.getLogger(__name__)
17
+
18
+
19
+ class FieldProtocol(Enum):
20
+ PROTOCOL_2030_5 = "2030.5"
21
+ PROTOCOL_DNP3 = "DNP3"
22
+
23
+
24
+ class SerializationProtocol(Enum):
25
+ JSON = "JSON"
26
+ XML = "XML"
27
+
28
+
29
+ class ConnectionType(Enum):
30
+ # VOLTTRON based connection
31
+ CONNECTION_TYPE_VOLTTRON = "VIP"
32
+ # Web Socket
33
+ # CONNECTION_TYPE_WS = "WS"
34
+ # CONNECTION_TYPE_HTTP = "HTTP"
35
+ # CONNECTION_TYPE_TCP = "TCP"
36
+ CONNECTION_TYPE_GRIDAPPSD = "CONNECTION_TYPE_GRIDAPPSD"
37
+
38
+
39
+ class ProtocolTransformer(ABC):
40
+ @staticmethod
41
+ @abstractmethod
42
+ def to_cim(data) -> str:
43
+ """
44
+ Create a cim message based upon the data passed for a given
45
+ concrete protocol. This is set as a static class because
46
+ all transformers should have this capability.
47
+
48
+ This function should return a string that can be manipulated
49
+ to go onto whatever message bus is necessary.
50
+ """
51
+ pass
52
+
53
+ @staticmethod
54
+ @abstractmethod
55
+ def to_protocol(cim_data: str, from_format: Optional[str] = None):
56
+ """
57
+ Change passed cim data into a protocol complient data stream
58
+ and return it.
59
+
60
+ cim_data: string representing cim data structures/change structure
61
+ from_format: specifies the type
62
+ """
63
+ pass
64
+
65
+
66
+
67
+ @dataclass
68
+ class MessageBusDefinition:
69
+ """
70
+ A `MessageBusDefinition` class is used to define how to connect to the
71
+ message bus.
72
+ """
73
+
74
+ """
75
+ A global unique string representing a specific message bus.
76
+ """
77
+ id: str
78
+
79
+ """
80
+ connection_type describes how the agent/endpoint will connect to the message bus
81
+ """
82
+ connection_type: ConnectionType
83
+
84
+ """
85
+ connection_args allows dynamic key/value paired strings to be added to allow connections.
86
+ """
87
+ conneciton_args: Dict[str, str]
88
+
89
+ """
90
+ Determines whether or not this message bus has the role of ot bus.
91
+ """
92
+ is_ot_bus: bool = False
93
+
94
+ @staticmethod
95
+ def load(config_file) -> MessageBusDefinition:
96
+ """
97
+
98
+ """
99
+ config = yaml.load(open(config_file),Loader=yaml.FullLoader)['connections']
100
+
101
+ required = ["id", "connection_type", "connection_args"]
102
+ for k in required:
103
+ if k not in config:
104
+ raise ValueError(f"Missing keys for connection {k}")
105
+
106
+ definition = MessageBusDefinition(config[required[0]], config[required[1]], config[required[2]])
107
+ for k in config:
108
+ if k == "connection_args":
109
+ definition.conneciton_args = dict()
110
+ for k1, v1 in config[k].items():
111
+ definition.conneciton_args[k1] = v1
112
+ else:
113
+ setattr(definition, k, config[k])
114
+
115
+ if not hasattr(definition, "is_ot_bus"):
116
+ setattr(definition, "is_ot_bus", False)
117
+
118
+ return definition
119
+
120
+
121
+ class FieldMessageBus:
122
+ def __init__(self, config: MessageBusDefinition):
123
+ self._devices = dict()
124
+ self._is_ot_bus = config.is_ot_bus
125
+ self._id = config.id
126
+
127
+ @property
128
+ def id(self):
129
+ return self._id
130
+
131
+ @property
132
+ def is_ot_bus(self):
133
+ return self._is_ot_bus
134
+
135
+ def add_device(self, device: "DeviceFieldInterface"):
136
+ self._devices[device.id] = device
137
+
138
+ def disconnect_device(self, id: str):
139
+ del self._devices[id]
140
+
141
+ @abstractmethod
142
+ def query_devices(self) -> dict:
143
+ pass
144
+
145
+ @abstractmethod
146
+ def is_connected(self) -> bool:
147
+ """
148
+ Is this object connected to the message bus
149
+ """
150
+ pass
151
+
152
+ @abstractmethod
153
+ def connect(self):
154
+ """
155
+ Connect to the concrete message bus that implements this interface.
156
+ """
157
+ pass
158
+
159
+ @abstractmethod
160
+ def subscribe(self, topic, callback):
161
+ pass
162
+
163
+ @abstractmethod
164
+ def unsubscribe(self, topic):
165
+ pass
166
+
167
+ @abstractmethod
168
+ def send(self, topic, message):
169
+ """
170
+ Publish device specific message to the concrete message bus.
171
+ """
172
+ pass
173
+
174
+ @abstractmethod
175
+ def get_response(self, topic, message, timeout):
176
+ """
177
+ Sends a message on a specific queue, waits and returns the response
178
+ """
179
+
180
+ def get_agent_response(self, agent_id, message, timeout):
181
+ """
182
+ Sends a message on a specific agent's request queue, waits and returns the response
183
+ """
184
+ topic = "{}.request.{}.{}".format(t.BASE_FIELD_QUEUE,self.id, agent_id)
185
+ self.get_response(topic, message, timeout)
186
+
187
+ @abstractmethod
188
+ def disconnect(self):
189
+ """
190
+ Disconnect the device from the concrete message bus.
191
+ """
192
+ pass
193
+
194
+
195
+ class MessageBusDefinitions:
196
+ def __init__(
197
+ self,
198
+ config: Optional[Union[dict, str]] = None,
199
+ yamlfile: Optional[Union[str, PathLike]] = None,
200
+ ):
201
+ self._buses = dict()
202
+
203
+ if config is None and yamlfile is None:
204
+ raise ValueError("Must have either config specified")
205
+
206
+ if config and yamlfile:
207
+ raise ValueError("Must have at least one of config or yamlfile specified.")
208
+
209
+ if yamlfile:
210
+ if not Path(yamlfile).exists():
211
+ raise ValueError(f"Invalid path for yamlfile {yamlfile}")
212
+ with open(yamlfile) as fp:
213
+ config = yaml.safe_load(fp)
214
+ elif isinstance(config, str):
215
+ config = yaml.load(config)
216
+
217
+ if config.get("connections"):
218
+ for con in config.get("connections"):
219
+ obj = MessageBusDefinition.load(con)
220
+ if self._buses.get(obj.id):
221
+ raise ValueError(f"Duplicate messagebus id specified for {obj.id}")
222
+ self._buses[obj.id] = obj
223
+ else:
224
+ obj = MessageBusDefinition.load(config)
225
+ self._buses[obj.id] = obj
226
+
227
+ self._iterator = None
228
+
229
+ def get(self, id: str) -> Union[MessageBusDefinition, None]:
230
+ return self._buses.get(id)
231
+
232
+ def __iter__(self):
233
+ if self._iterator is None:
234
+ self._iterator = iter(self._buses)
235
+ return self._iterator
236
+
237
+ def __next__(self) -> MessageBusDefinition:
238
+ try:
239
+ definition = next(self._iterator)
240
+ except StopIteration:
241
+ self._iterator = None
242
+ return None
243
+ else:
244
+ return definition
245
+
246
+
247
+ class DeviceFieldInterface:
248
+ def __init__(
249
+ self,
250
+ id: str,
251
+ field_bus: FieldMessageBus,
252
+ publish_topic: str,
253
+ control_topic: str,
254
+ ):
255
+ self._id = id
256
+ self._field_bus = field_bus
257
+ self._publish_topic = publish_topic
258
+ self._control_topic = control_topic
259
+ self._running = False
260
+
261
+ @property
262
+ def id(self):
263
+ return self._id
264
+
265
+ @property
266
+ def publish_topic(self):
267
+ return self._publish_topic
268
+
269
+ @property
270
+ def control_topic(self):
271
+ return self._control_topic
272
+
273
+ def publish_field_bus(self, cim_data):
274
+ self._field_bus.publish(topic=self._publish_topic, data=cim_data)
275
+
276
+ @abstractmethod
277
+ def on_control_message(self, message):
278
+ pass
279
+
280
+ @abstractmethod
281
+ def on_state_change(self):
282
+ """
283
+ This event should be triggered by the device/protocol that
284
+ is used.
285
+ """
286
+ pass
@@ -0,0 +1,39 @@
1
+ [tool.poetry]
2
+ name = "gridappsd-field-bus"
3
+ version = "2023.5"
4
+ description = "GridAPPS-D Field Bus Implementation"
5
+ authors = [
6
+ "C. Allwardt <3979063+craig8@users.noreply.github.com>",
7
+ "P. Sharma <poorva.sharma@pnnl.gov",
8
+ "A. Fisher <andrew.fisher@pnnl.gov"
9
+ ]
10
+ license = "BSD-3-Clause"
11
+
12
+ repository = "https://github.com/GRIDAPPSD/gridappsd-python"
13
+ homepage = "https://gridappsd.readthedocs.io"
14
+
15
+ keywords = ["gridappsd", "grid", "activmq", "powergrid", "simulation", "library"]
16
+
17
+ readme = "README.md"
18
+
19
+
20
+ packages = [
21
+ { include = 'gridappsd'}
22
+ ]
23
+
24
+
25
+ [tool.poetry.dependencies]
26
+ python = ">=3.7.9,<4.0"
27
+ gridappsd-python = "^2023.5"
28
+ cim-graph = "^2023.5.1a3"
29
+
30
+ [tool.poetry.group.dev.dependencies]
31
+ pytest = "^6.2.2"
32
+ pytest-html = "^3.1.1"
33
+ mock = "^4.0.3"
34
+ docker = "^4.4.4"
35
+ yapf = "^0.32.0"
36
+
37
+ [build-system]
38
+ requires = ["poetry-core>=1.2.0"]
39
+ build-backend = "poetry.core.masonry.api"