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.
- gridappsd_field_bus-2023.5/PKG-INFO +22 -0
- gridappsd_field_bus-2023.5/README.md +0 -0
- gridappsd_field_bus-2023.5/gridappsd/__no_init__here +0 -0
- gridappsd_field_bus-2023.5/gridappsd/field_interface/__init__.py +9 -0
- gridappsd_field_bus-2023.5/gridappsd/field_interface/agents/__init__.py +15 -0
- gridappsd_field_bus-2023.5/gridappsd/field_interface/agents/agents.py +353 -0
- gridappsd_field_bus-2023.5/gridappsd/field_interface/context.py +51 -0
- gridappsd_field_bus-2023.5/gridappsd/field_interface/gridappsd_field_bus.py +58 -0
- gridappsd_field_bus-2023.5/gridappsd/field_interface/interfaces.py +286 -0
- gridappsd_field_bus-2023.5/pyproject.toml +39 -0
|
@@ -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,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"
|