gridappsd-python 2024.1.1a2__tar.gz → 2024.4.0a0__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_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/PKG-INFO +9 -10
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/README.md +8 -9
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/__init__.py +0 -2
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/__main__.py +5 -3
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/app_registration.py +8 -12
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/difference_builder.py +1 -1
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/goss.py +35 -22
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/gridappsd.py +29 -30
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/houses.py +7 -17
- gridappsd_python-2024.4.0a0/gridappsd/json_extension.py +123 -0
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/loghandler.py +6 -5
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/register_app.py +5 -4
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/simulation.py +21 -29
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/timeseries.py +34 -34
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/topics.py +25 -15
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/utils.py +2 -4
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/pyproject.toml +2 -2
- gridappsd_python-2024.1.1a2/gridappsd/json_extension.py +0 -55
- {gridappsd_python-2024.1.1a2 → gridappsd_python-2024.4.0a0}/gridappsd/docker_handler.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: gridappsd-python
|
|
3
|
-
Version: 2024.
|
|
3
|
+
Version: 2024.4.0a0
|
|
4
4
|
Summary: A GridAPPS-D Python Adapter
|
|
5
5
|
Home-page: https://gridappsd.readthedocs.io
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -203,9 +203,9 @@ integration tests available to be run. In order to execute these, you must have
|
|
|
203
203
|
as above with dev-dependencies.
|
|
204
204
|
|
|
205
205
|
During the testing phase the docker containers required for the tests are downloaded from
|
|
206
|
-
dockerhub and started. By default the `develop` tag is used to test the library using pytest.
|
|
206
|
+
dockerhub and started. By default the `develop` tag is used to test the library using pytest.
|
|
207
207
|
One can customize the docker image tag by setting the environmental
|
|
208
|
-
variable `GRIDAPPSD_TAG_ENV` either by `export GRIDAPPSD_TAG_ENV=other_tag` or by executing
|
|
208
|
+
variable `GRIDAPPSD_TAG_ENV` either by `export GRIDAPPSD_TAG_ENV=other_tag` or by executing
|
|
209
209
|
pytest with the following:
|
|
210
210
|
|
|
211
211
|
```shell script
|
|
@@ -215,7 +215,7 @@ pytest with the following:
|
|
|
215
215
|
export GRIDAPPSD_TAG_ENV=other_tag
|
|
216
216
|
pytest
|
|
217
217
|
|
|
218
|
-
# Tests also require the username and password to be avaialable as environmental variables
|
|
218
|
+
# Tests also require the username and password to be avaialable as environmental variables
|
|
219
219
|
# in order for them to properly run these tests
|
|
220
220
|
export GRIDAPPSD_USER=user
|
|
221
221
|
export GRIDAPPSD_PASSWORD=pass
|
|
@@ -225,18 +225,18 @@ pytest
|
|
|
225
225
|
|
|
226
226
|
***NOTE: the first running the tests will download all of the docker images associated with the
|
|
227
227
|
[GOSS-GridAPPS-D](http://github.com/GRIDAPPSD/GOSS-GridAPPS-D) repository. This process may take some time.***
|
|
228
|
-
|
|
228
|
+
|
|
229
229
|
### Running tests created in a new project
|
|
230
230
|
|
|
231
231
|
The `gridappsd-python` library exposes a testing environment through the `gridappsd.docker_handler` module. Including the following
|
|
232
232
|
`conftest.py` in the root of your base test directory allows tests to reference these. Using these fixtures will start all of the
|
|
233
|
-
base containers required for `gridappsd` to run.
|
|
233
|
+
base containers required for `gridappsd` to run.
|
|
234
234
|
|
|
235
235
|
```python
|
|
236
236
|
|
|
237
237
|
# conftest.py
|
|
238
|
-
# Create a conftest.py file in the root of the tests directory to enable usage throughout the tests directory and below.
|
|
239
|
-
#
|
|
238
|
+
# Create a conftest.py file in the root of the tests directory to enable usage throughout the tests directory and below.
|
|
239
|
+
#
|
|
240
240
|
# Tested project structure an layout
|
|
241
241
|
#
|
|
242
242
|
# project-folder\
|
|
@@ -328,7 +328,7 @@ Using the above fixtures from inside a test module and test function looks like
|
|
|
328
328
|
|
|
329
329
|
```python
|
|
330
330
|
|
|
331
|
-
# Example test function using the gridappsd_client fixture
|
|
331
|
+
# Example test function using the gridappsd_client fixture
|
|
332
332
|
|
|
333
333
|
@mock.patch.dict(os.environ, {"GRIDAPPSD_APPLICATION_ID": "helics_goss_bridge.py"})
|
|
334
334
|
def test_gridappsd_status(gridappsd_client):
|
|
@@ -351,4 +351,3 @@ def test_gridappsd_status(gridappsd_client):
|
|
|
351
351
|
assert gappsd.get_application_status() == ProcessStatusEnum.COMPLETE.value
|
|
352
352
|
```
|
|
353
353
|
|
|
354
|
-
|
|
@@ -178,9 +178,9 @@ integration tests available to be run. In order to execute these, you must have
|
|
|
178
178
|
as above with dev-dependencies.
|
|
179
179
|
|
|
180
180
|
During the testing phase the docker containers required for the tests are downloaded from
|
|
181
|
-
dockerhub and started. By default the `develop` tag is used to test the library using pytest.
|
|
181
|
+
dockerhub and started. By default the `develop` tag is used to test the library using pytest.
|
|
182
182
|
One can customize the docker image tag by setting the environmental
|
|
183
|
-
variable `GRIDAPPSD_TAG_ENV` either by `export GRIDAPPSD_TAG_ENV=other_tag` or by executing
|
|
183
|
+
variable `GRIDAPPSD_TAG_ENV` either by `export GRIDAPPSD_TAG_ENV=other_tag` or by executing
|
|
184
184
|
pytest with the following:
|
|
185
185
|
|
|
186
186
|
```shell script
|
|
@@ -190,7 +190,7 @@ pytest with the following:
|
|
|
190
190
|
export GRIDAPPSD_TAG_ENV=other_tag
|
|
191
191
|
pytest
|
|
192
192
|
|
|
193
|
-
# Tests also require the username and password to be avaialable as environmental variables
|
|
193
|
+
# Tests also require the username and password to be avaialable as environmental variables
|
|
194
194
|
# in order for them to properly run these tests
|
|
195
195
|
export GRIDAPPSD_USER=user
|
|
196
196
|
export GRIDAPPSD_PASSWORD=pass
|
|
@@ -200,18 +200,18 @@ pytest
|
|
|
200
200
|
|
|
201
201
|
***NOTE: the first running the tests will download all of the docker images associated with the
|
|
202
202
|
[GOSS-GridAPPS-D](http://github.com/GRIDAPPSD/GOSS-GridAPPS-D) repository. This process may take some time.***
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
### Running tests created in a new project
|
|
205
205
|
|
|
206
206
|
The `gridappsd-python` library exposes a testing environment through the `gridappsd.docker_handler` module. Including the following
|
|
207
207
|
`conftest.py` in the root of your base test directory allows tests to reference these. Using these fixtures will start all of the
|
|
208
|
-
base containers required for `gridappsd` to run.
|
|
208
|
+
base containers required for `gridappsd` to run.
|
|
209
209
|
|
|
210
210
|
```python
|
|
211
211
|
|
|
212
212
|
# conftest.py
|
|
213
|
-
# Create a conftest.py file in the root of the tests directory to enable usage throughout the tests directory and below.
|
|
214
|
-
#
|
|
213
|
+
# Create a conftest.py file in the root of the tests directory to enable usage throughout the tests directory and below.
|
|
214
|
+
#
|
|
215
215
|
# Tested project structure an layout
|
|
216
216
|
#
|
|
217
217
|
# project-folder\
|
|
@@ -303,7 +303,7 @@ Using the above fixtures from inside a test module and test function looks like
|
|
|
303
303
|
|
|
304
304
|
```python
|
|
305
305
|
|
|
306
|
-
# Example test function using the gridappsd_client fixture
|
|
306
|
+
# Example test function using the gridappsd_client fixture
|
|
307
307
|
|
|
308
308
|
@mock.patch.dict(os.environ, {"GRIDAPPSD_APPLICATION_ID": "helics_goss_bridge.py"})
|
|
309
309
|
def test_gridappsd_status(gridappsd_client):
|
|
@@ -325,4 +325,3 @@ def test_gridappsd_status(gridappsd_client):
|
|
|
325
325
|
assert gappsd.get_service_status() == ProcessStatusEnum.COMPLETE.value
|
|
326
326
|
assert gappsd.get_application_status() == ProcessStatusEnum.COMPLETE.value
|
|
327
327
|
```
|
|
328
|
-
|
|
@@ -49,19 +49,21 @@ from gridappsd import GridAPPSD
|
|
|
49
49
|
|
|
50
50
|
assert sys.version_info >= (3, 6), "Minimum version is python 3.6"
|
|
51
51
|
|
|
52
|
-
logging.basicConfig(stream=sys.stdout,
|
|
52
|
+
logging.basicConfig(stream=sys.stdout,
|
|
53
|
+
level=logging.INFO,
|
|
53
54
|
format="'%(asctime)s: %(name)-20s - %(levelname)-6s - %(message)s")
|
|
54
55
|
|
|
55
56
|
logging.getLogger('stomp.py').setLevel(logging.WARNING)
|
|
56
57
|
_log = logging.getLogger("gridappsd.__main__")
|
|
57
58
|
|
|
58
|
-
|
|
59
59
|
if __name__ == '__main__':
|
|
60
60
|
|
|
61
61
|
parser = ArgumentParser()
|
|
62
62
|
|
|
63
63
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
64
|
-
group.add_argument("-s",
|
|
64
|
+
group.add_argument("-s",
|
|
65
|
+
"--run-simulation",
|
|
66
|
+
type=argparse.FileType('r'),
|
|
65
67
|
help="Start running a simulation from a passed simulation file.")
|
|
66
68
|
|
|
67
69
|
opts = parser.parse_args()
|
|
@@ -13,8 +13,8 @@ import shlex
|
|
|
13
13
|
import sys
|
|
14
14
|
import os
|
|
15
15
|
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
16
|
+
from .gridappsd import GridAPPSD
|
|
17
|
+
from .topics import REQUEST_REGISTER_APP
|
|
18
18
|
from . import utils, json_extension as json
|
|
19
19
|
|
|
20
20
|
_log = logging.getLogger(__name__)
|
|
@@ -26,6 +26,7 @@ if os.name == 'posix':
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class Job(threading.Thread):
|
|
29
|
+
|
|
29
30
|
def __init__(self, args, out=sys.stdout, err=sys.stderr):
|
|
30
31
|
threading.Thread.__init__(self)
|
|
31
32
|
_log.debug("Creating job")
|
|
@@ -42,10 +43,7 @@ class Job(threading.Thread):
|
|
|
42
43
|
self.running = True
|
|
43
44
|
os.environ['GRIDAPPSD_APPLICATION_STATUS'] = 'RUNNING'
|
|
44
45
|
|
|
45
|
-
p = subprocess.Popen(args=self._args,
|
|
46
|
-
shell=False,
|
|
47
|
-
stdout=self._out,
|
|
48
|
-
stderr=self._err)
|
|
46
|
+
p = subprocess.Popen(args=self._args, shell=False, stdout=self._out, stderr=self._err)
|
|
49
47
|
|
|
50
48
|
# Loop while process is executing
|
|
51
49
|
while p.poll() is None and self.running:
|
|
@@ -112,13 +110,10 @@ class ApplicationController(object):
|
|
|
112
110
|
return self._heartbeat_thread is not None
|
|
113
111
|
|
|
114
112
|
def register_app(self, end_callback):
|
|
115
|
-
print("Sending {}\n\tto {}".format(self._configDict,
|
|
116
|
-
REQUEST_REGISTER_APP))
|
|
113
|
+
print("Sending {}\n\tto {}".format(self._configDict, REQUEST_REGISTER_APP))
|
|
117
114
|
self._gapd.get_logger().debug("Started App Registration")
|
|
118
115
|
|
|
119
|
-
response = self._gapd.get_response(REQUEST_REGISTER_APP,
|
|
120
|
-
self._configDict,
|
|
121
|
-
60)
|
|
116
|
+
response = self._gapd.get_response(REQUEST_REGISTER_APP, self._configDict, 60)
|
|
122
117
|
if 'message' in response:
|
|
123
118
|
_log.error("An error regisering the application occured")
|
|
124
119
|
_log.error(response.get('message'))
|
|
@@ -156,7 +151,8 @@ class ApplicationController(object):
|
|
|
156
151
|
# print("Seanding heartbeat {} {}".format(self._heartbeat_topic, self._application_id))
|
|
157
152
|
# print("Heartbeat period: {}".format(self._heartbeat_period))
|
|
158
153
|
self._gapd.send(self._heartbeat_topic, self._application_id)
|
|
159
|
-
time.sleep(self._heartbeat_period -
|
|
154
|
+
time.sleep(self._heartbeat_period -
|
|
155
|
+
((time.time() - starttime) % self._heartbeat_period))
|
|
160
156
|
except:
|
|
161
157
|
error_callback()
|
|
162
158
|
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
# PACIFIC NORTHWEST NATIONAL LABORATORY operated by BATTELLE for the
|
|
38
38
|
# UNITED STATES DEPARTMENT OF ENERGY under Contract DE-AC05-76RL01830
|
|
39
39
|
# -------------------------------------------------------------------------------
|
|
40
|
-
|
|
41
40
|
"""
|
|
42
41
|
Created on March 1, 2018
|
|
43
42
|
|
|
@@ -81,10 +80,14 @@ class GOSS(object):
|
|
|
81
80
|
""" Base class providing connections to a GOSS instance via stomp protocol
|
|
82
81
|
"""
|
|
83
82
|
|
|
84
|
-
def __init__(self,
|
|
85
|
-
|
|
83
|
+
def __init__(self,
|
|
84
|
+
username=None,
|
|
85
|
+
password=None,
|
|
86
|
+
stomp_address='localhost',
|
|
87
|
+
stomp_port='61613',
|
|
86
88
|
attempt_connection=True,
|
|
87
|
-
override_threading=None,
|
|
89
|
+
override_threading=None,
|
|
90
|
+
stomp_log_level=logging.WARNING,
|
|
88
91
|
goss_log_level=logging.INFO):
|
|
89
92
|
|
|
90
93
|
logging.getLogger('stomp.py').setLevel(stomp_log_level)
|
|
@@ -146,27 +149,31 @@ class GOSS(object):
|
|
|
146
149
|
if isinstance(message, list) or isinstance(message, dict):
|
|
147
150
|
message = json.dumps(message)
|
|
148
151
|
_log.debug("Sending topic: {} body: {}".format(topic, message))
|
|
149
|
-
self._conn.send(body=message,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
self._conn.send(body=message,
|
|
153
|
+
destination=topic,
|
|
154
|
+
headers={
|
|
155
|
+
'GOSS_HAS_SUBJECT': True,
|
|
156
|
+
'GOSS_SUBJECT': self.__token
|
|
157
|
+
})
|
|
158
|
+
|
|
153
159
|
def get_response(self, topic, message, timeout=5):
|
|
154
|
-
id = datetime.now().strftime("%Y%m%d%h%M%S")
|
|
160
|
+
id = datetime.now().strftime("%Y%m%d%h%M%S%f")[:-3]
|
|
155
161
|
reply_to = "/temp-queue/response.{}".format(id)
|
|
156
|
-
|
|
162
|
+
|
|
157
163
|
if isinstance(message, str):
|
|
158
164
|
message = json.loads(message)
|
|
159
|
-
|
|
165
|
+
|
|
160
166
|
if 'resultFormat' in message:
|
|
161
|
-
self.result_format = message['resultFormat']
|
|
167
|
+
self.result_format = message['resultFormat']
|
|
162
168
|
|
|
163
169
|
# Change message to string if we have a dictionary.
|
|
164
170
|
if isinstance(message, dict):
|
|
165
171
|
message = json.dumps(message)
|
|
166
172
|
elif isinstance(message, list):
|
|
167
173
|
message = json.dumps(message)
|
|
168
|
-
|
|
174
|
+
|
|
169
175
|
class ResponseListener(object):
|
|
176
|
+
|
|
170
177
|
def __init__(self, topic, result_format):
|
|
171
178
|
self.response = None
|
|
172
179
|
self._topic = topic
|
|
@@ -181,7 +188,7 @@ class GOSS(object):
|
|
|
181
188
|
self.response = message
|
|
182
189
|
else:
|
|
183
190
|
self.response = json.loads(message)
|
|
184
|
-
else:
|
|
191
|
+
else:
|
|
185
192
|
self.response = message
|
|
186
193
|
except ValueError:
|
|
187
194
|
self.response = dict(error="Invalid json returned",
|
|
@@ -198,9 +205,13 @@ class GOSS(object):
|
|
|
198
205
|
listener = ResponseListener(reply_to, self.result_format)
|
|
199
206
|
self.subscribe(reply_to, listener)
|
|
200
207
|
|
|
201
|
-
self._conn.send(body=message,
|
|
202
|
-
|
|
203
|
-
|
|
208
|
+
self._conn.send(body=message,
|
|
209
|
+
destination=topic,
|
|
210
|
+
headers={
|
|
211
|
+
'reply-to': reply_to,
|
|
212
|
+
'GOSS_HAS_SUBJECT': True,
|
|
213
|
+
'GOSS_SUBJECT': self.__token
|
|
214
|
+
})
|
|
204
215
|
count = 0
|
|
205
216
|
|
|
206
217
|
while count < timeout:
|
|
@@ -282,7 +293,7 @@ class GOSS(object):
|
|
|
282
293
|
|
|
283
294
|
# get token
|
|
284
295
|
# get initial connection
|
|
285
|
-
dt=datetime.now()
|
|
296
|
+
dt = datetime.now()
|
|
286
297
|
replyDest = f"temp.token_resp.{self.__user__}-{dt}"
|
|
287
298
|
|
|
288
299
|
# create token request string
|
|
@@ -297,8 +308,9 @@ class GOSS(object):
|
|
|
297
308
|
if self._override_thread_fc is not None:
|
|
298
309
|
tmpConn.transport.override_threading(self._override_thread_fc)
|
|
299
310
|
tmpConn.connect(self.__user__, self.__pass__, wait=True)
|
|
300
|
-
|
|
311
|
+
|
|
301
312
|
class TokenResponseListener():
|
|
313
|
+
|
|
302
314
|
def __init__(self):
|
|
303
315
|
self.__token = None
|
|
304
316
|
|
|
@@ -307,7 +319,7 @@ class GOSS(object):
|
|
|
307
319
|
|
|
308
320
|
def on_message(self, header, message):
|
|
309
321
|
_log.debug("Internal on message is: {} {}".format(header, message))
|
|
310
|
-
|
|
322
|
+
|
|
311
323
|
self.__token = str(message)
|
|
312
324
|
|
|
313
325
|
def on_error(self, headers, message):
|
|
@@ -322,9 +334,10 @@ class GOSS(object):
|
|
|
322
334
|
listener = TokenResponseListener()
|
|
323
335
|
|
|
324
336
|
# self.subscribe(replyDest, listener)
|
|
325
|
-
tmpConn.subscribe('/queue/'+replyDest, 123)
|
|
337
|
+
tmpConn.subscribe('/queue/' + replyDest, 123)
|
|
326
338
|
tmpConn.set_listener('token_resp', listener)
|
|
327
|
-
tmpConn.send(body=base64Str,
|
|
339
|
+
tmpConn.send(body=base64Str,
|
|
340
|
+
destination=tokenTopic,
|
|
328
341
|
headers={'reply-to': replyDest})
|
|
329
342
|
# while token is null or for x iterations
|
|
330
343
|
iter = 0
|
|
@@ -65,9 +65,9 @@ class InvalidSimulationIdError(Exception):
|
|
|
65
65
|
class GridAPPSD(GOSS):
|
|
66
66
|
""" The main :class:`GridAPPSD` interface for connecting to a GridAPPSD instance
|
|
67
67
|
"""
|
|
68
|
+
|
|
68
69
|
# TODO Get the caller from the traceback/inspect module.
|
|
69
|
-
def __init__(self, simulation_id=None,
|
|
70
|
-
address=None, **kwargs):
|
|
70
|
+
def __init__(self, simulation_id=None, address=None, **kwargs):
|
|
71
71
|
|
|
72
72
|
if address is None:
|
|
73
73
|
address = utils.get_gridappsd_address()
|
|
@@ -78,10 +78,7 @@ class GridAPPSD(GOSS):
|
|
|
78
78
|
'stomp_port' in kwargs and not 'stomp_address' in kwargs:
|
|
79
79
|
raise ValueError("If stomp_address is specified the so should stomp_port")
|
|
80
80
|
|
|
81
|
-
super(GridAPPSD, self).__init__(
|
|
82
|
-
stomp_address=address[0],
|
|
83
|
-
stomp_port=address[1],
|
|
84
|
-
**kwargs)
|
|
81
|
+
super(GridAPPSD, self).__init__(stomp_address=address[0], stomp_port=address[1], **kwargs)
|
|
85
82
|
self._houses = Houses(self)
|
|
86
83
|
self._simulation_log_topic = None
|
|
87
84
|
self._simulation_id = None
|
|
@@ -104,11 +101,11 @@ class GridAPPSD(GOSS):
|
|
|
104
101
|
|
|
105
102
|
def get_houses(self) -> Houses:
|
|
106
103
|
return self._houses
|
|
107
|
-
|
|
104
|
+
|
|
108
105
|
def get_simulation_id(self):
|
|
109
|
-
"""
|
|
106
|
+
"""
|
|
110
107
|
Return the simulation_id given to the GridAPPSD instance
|
|
111
|
-
|
|
108
|
+
|
|
112
109
|
:return:
|
|
113
110
|
"""
|
|
114
111
|
if not self._simulation_id:
|
|
@@ -116,7 +113,7 @@ class GridAPPSD(GOSS):
|
|
|
116
113
|
if self._simulation_id:
|
|
117
114
|
self._simulation_log_topic = t.simulation_log_topic(self._simulation_id)
|
|
118
115
|
return self._simulation_id
|
|
119
|
-
|
|
116
|
+
|
|
120
117
|
def set_application_status(self, status):
|
|
121
118
|
"""
|
|
122
119
|
Set the application status.
|
|
@@ -125,8 +122,8 @@ class GridAPPSD(GOSS):
|
|
|
125
122
|
try:
|
|
126
123
|
self._process_status = ProcessStatusEnum(status)
|
|
127
124
|
except ValueError:
|
|
128
|
-
self.get_logger().warning("Unsuccessful change of application status."
|
|
129
|
-
|
|
125
|
+
self.get_logger().warning("Unsuccessful change of application status." +
|
|
126
|
+
f"Valid statuses are {ProcessStatusEnum.__members__}.")
|
|
130
127
|
|
|
131
128
|
def set_service_status(self, status):
|
|
132
129
|
"""
|
|
@@ -136,8 +133,8 @@ class GridAPPSD(GOSS):
|
|
|
136
133
|
try:
|
|
137
134
|
self._process_status = ProcessStatusEnum(status)
|
|
138
135
|
except ValueError:
|
|
139
|
-
self.get_logger().warning("Unsuccessful change of service status."
|
|
140
|
-
|
|
136
|
+
self.get_logger().warning("Unsuccessful change of service status." +
|
|
137
|
+
f"Valid statuses are {ProcessStatusEnum.__members__}.")
|
|
141
138
|
|
|
142
139
|
def set_simulation_id(self, simulation_id):
|
|
143
140
|
if simulation_id is None:
|
|
@@ -156,17 +153,17 @@ class GridAPPSD(GOSS):
|
|
|
156
153
|
|
|
157
154
|
def get_application_id(self):
|
|
158
155
|
return utils.get_gridappsd_application_id()
|
|
159
|
-
|
|
156
|
+
|
|
160
157
|
def get_service_status(self):
|
|
161
158
|
"""
|
|
162
159
|
Return the service status
|
|
163
160
|
:return:
|
|
164
161
|
"""
|
|
165
162
|
return self._process_status.value
|
|
166
|
-
|
|
163
|
+
|
|
167
164
|
def query_object_types(self, model_id=None):
|
|
168
165
|
""" Allows the caller to query the different object types.
|
|
169
|
-
|
|
166
|
+
|
|
170
167
|
:param model_id:
|
|
171
168
|
:return:
|
|
172
169
|
"""
|
|
@@ -186,7 +183,7 @@ class GridAPPSD(GOSS):
|
|
|
186
183
|
def query_model_info(self):
|
|
187
184
|
payload = self._build_query_payload("QUERY_MODEL_INFO")
|
|
188
185
|
return self.get_response(t.REQUEST_POWERGRID_DATA, payload, timeout=30)
|
|
189
|
-
|
|
186
|
+
|
|
190
187
|
def query_model(self, model_id=None, object_type=None, object_id=None, response_format='JSON'):
|
|
191
188
|
args = {}
|
|
192
189
|
if model_id is not None:
|
|
@@ -194,9 +191,9 @@ class GridAPPSD(GOSS):
|
|
|
194
191
|
if object_type is not None:
|
|
195
192
|
args["objectType"] = object_type
|
|
196
193
|
if object_id is not None:
|
|
197
|
-
args["objectId"] = object_id
|
|
194
|
+
args["objectId"] = object_id
|
|
198
195
|
if response_format is not None:
|
|
199
|
-
args["resultFormat"] = response_format
|
|
196
|
+
args["resultFormat"] = response_format
|
|
200
197
|
payload = self._build_query_payload("QUERY_MODEL", **args)
|
|
201
198
|
return self.get_response(t.REQUEST_POWERGRID_DATA, payload, timeout=30)
|
|
202
199
|
|
|
@@ -208,13 +205,13 @@ class GridAPPSD(GOSS):
|
|
|
208
205
|
args["modelId"] = model_id
|
|
209
206
|
payload = self._build_query_payload("QUERY_OBJECT", **args)
|
|
210
207
|
return self.get_response(t.REQUEST_POWERGRID_DATA, payload, timeout=30)
|
|
211
|
-
|
|
208
|
+
|
|
212
209
|
def query_object_dictionary(self, model_id, object_type=None, object_id=None):
|
|
213
210
|
if not model_id:
|
|
214
211
|
raise ValueError("model_id is not specified.")
|
|
215
212
|
if not object_id and not object_type:
|
|
216
213
|
raise ValueError("No obejct_id or object_type specified.")
|
|
217
|
-
args={}
|
|
214
|
+
args = {}
|
|
218
215
|
args["modelId"] = model_id
|
|
219
216
|
if object_id is not None:
|
|
220
217
|
args["objectId"] = object_id
|
|
@@ -235,9 +232,15 @@ class GridAPPSD(GOSS):
|
|
|
235
232
|
request_topic = '.'.join((t.REQUEST_DATA, database_type))
|
|
236
233
|
return self.get_response(request_topic, json.dumps(payload), timeout=timeout)
|
|
237
234
|
|
|
238
|
-
def get_platform_status(self,
|
|
235
|
+
def get_platform_status(self,
|
|
236
|
+
applications=True,
|
|
237
|
+
services=True,
|
|
238
|
+
appInstances=True,
|
|
239
|
+
serviceInstances=True):
|
|
239
240
|
_log.debug("Retrieving platform status from GridAPPSD")
|
|
240
|
-
msg = dict(appInstances=appInstances,
|
|
241
|
+
msg = dict(appInstances=appInstances,
|
|
242
|
+
applications=applications,
|
|
243
|
+
services=services,
|
|
241
244
|
serviceInstances=serviceInstances)
|
|
242
245
|
return self.get_response(t.REQUEST_PLATFORM_STATUS, json.dumps(msg), timeout=30)
|
|
243
246
|
|
|
@@ -256,8 +259,7 @@ class GridAPPSD(GOSS):
|
|
|
256
259
|
|
|
257
260
|
def send_status(self, status, topic, log_level=INFO):
|
|
258
261
|
self.set_application_status(status)
|
|
259
|
-
status_message = self.build_message_json(self.get_application_status(),
|
|
260
|
-
"", log_level)
|
|
262
|
+
status_message = self.build_message_json(self.get_application_status(), "", log_level)
|
|
261
263
|
self.send(topic, status_message)
|
|
262
264
|
|
|
263
265
|
def build_message_json(self, status, message, log_level):
|
|
@@ -265,7 +267,7 @@ class GridAPPSD(GOSS):
|
|
|
265
267
|
status_message = {
|
|
266
268
|
"source": utils.get_gridappsd_application_id(),
|
|
267
269
|
"processId": f"{self._simulation_id}",
|
|
268
|
-
"timestamp": int(time.mktime(t_now.timetuple()))*1000,
|
|
270
|
+
"timestamp": int(time.mktime(t_now.timetuple())) * 1000,
|
|
269
271
|
"procesStatus": status,
|
|
270
272
|
"logMessage": str(message),
|
|
271
273
|
"logLevel": logging.getLevelName(log_level),
|
|
@@ -279,6 +281,3 @@ class GridAPPSD(GOSS):
|
|
|
279
281
|
d = dict(requestType=request_type, resultFormat=response_format)
|
|
280
282
|
d.update(**kwargs)
|
|
281
283
|
return d
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
from collections import namedtuple
|
|
2
2
|
|
|
3
3
|
house_keys = [
|
|
4
|
-
'name',
|
|
5
|
-
'
|
|
6
|
-
'coolingSetpoint',
|
|
7
|
-
'coolingSystem',
|
|
8
|
-
'floorArea',
|
|
9
|
-
'heatingSetpoint',
|
|
10
|
-
'heatingSystem',
|
|
11
|
-
'hvacPowerFactor',
|
|
12
|
-
'numberOfStories',
|
|
13
|
-
'thermalIntegrity',
|
|
14
|
-
'id',
|
|
15
|
-
'fdrid'
|
|
4
|
+
'name', 'parent', 'coolingSetpoint', 'coolingSystem', 'floorArea', 'heatingSetpoint',
|
|
5
|
+
'heatingSystem', 'hvacPowerFactor', 'numberOfStories', 'thermalIntegrity', 'id', 'fdrid'
|
|
16
6
|
]
|
|
17
7
|
House = namedtuple('House', house_keys)
|
|
18
8
|
|
|
@@ -21,7 +11,9 @@ House = namedtuple('House', house_keys)
|
|
|
21
11
|
|
|
22
12
|
|
|
23
13
|
class Houses:
|
|
14
|
+
|
|
24
15
|
class __SingltonHouses:
|
|
16
|
+
|
|
25
17
|
def __init__(self, gappsd: 'GridAPPSD'):
|
|
26
18
|
self._gappsd = gappsd
|
|
27
19
|
self._houses = {}
|
|
@@ -38,8 +30,8 @@ class Houses:
|
|
|
38
30
|
query = """# list houses - DistHouse
|
|
39
31
|
PREFIX r: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
40
32
|
PREFIX c: <http://iec.ch/TC57/CIM100#>
|
|
41
|
-
SELECT ?fdrname ?name ?parent ?coolingSetpoint ?coolingSystem ?floorArea ?heatingSetpoint ?heatingSystem ?hvacPowerFactor ?numberOfStories ?thermalIntegrity ?id ?fdrid
|
|
42
|
-
WHERE {
|
|
33
|
+
SELECT ?fdrname ?name ?parent ?coolingSetpoint ?coolingSystem ?floorArea ?heatingSetpoint ?heatingSystem ?hvacPowerFactor ?numberOfStories ?thermalIntegrity ?id ?fdrid
|
|
34
|
+
WHERE {
|
|
43
35
|
# VALUES ?fdrid {"_9CE150A8-8CC5-A0F9-B67E-BBD8C79D3095"} # R2 12.47 3
|
|
44
36
|
# VALUES ?fdrid {"_4F76A5F9-271D-9EB8-5E31-AA362D86F2C3"} # 8500-node
|
|
45
37
|
# VALUES ?fdrid {"_E407CBB6-8C8D-9BC9-589C-AB83FBF0826D"} # 123 PV/Triplex
|
|
@@ -54,7 +46,7 @@ WHERE {
|
|
|
54
46
|
OPTIONAL{?h c:House.heatingSetpoint ?heatingSetpoint.}
|
|
55
47
|
OPTIONAL{?h c:House.hvacPowerFactor ?hvacPowerFactor.}
|
|
56
48
|
?h c:House.coolingSystem ?coolingSystemRaw.
|
|
57
|
-
bind(strafter(str(?coolingSystemRaw),"HouseCooling.") as ?coolingSystem)
|
|
49
|
+
bind(strafter(str(?coolingSystemRaw),"HouseCooling.") as ?coolingSystem)
|
|
58
50
|
?h c:House.heatingSystem ?heatingSystemRaw.
|
|
59
51
|
bind(strafter(str(?heatingSystemRaw),"HouseHeating.") as ?heatingSystem)
|
|
60
52
|
?h c:House.thermalIntegrity ?thermalIntegrityRaw.
|
|
@@ -83,7 +75,6 @@ WHERE {
|
|
|
83
75
|
|
|
84
76
|
self._houses[feeder] = houses
|
|
85
77
|
|
|
86
|
-
|
|
87
78
|
instance = None
|
|
88
79
|
|
|
89
80
|
def __init__(self, gappsd):
|
|
@@ -94,4 +85,3 @@ WHERE {
|
|
|
94
85
|
|
|
95
86
|
def __getattr__(self, name):
|
|
96
87
|
return getattr(self.instance, name)
|
|
97
|
-
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json as _json
|
|
3
|
+
from typing import Any, TextIO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclasses.dataclass
|
|
7
|
+
class JsonComplex:
|
|
8
|
+
real: float
|
|
9
|
+
imag: float
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def jsonDecoderExtension(obj: Any):
|
|
13
|
+
rv = obj
|
|
14
|
+
if isinstance(obj, dict):
|
|
15
|
+
try:
|
|
16
|
+
complexInstance = JsonComplex(**obj)
|
|
17
|
+
rv = complex(complexInstance.real, complexInstance.imag)
|
|
18
|
+
except:
|
|
19
|
+
rv = obj
|
|
20
|
+
return rv
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JsonEncoderExtension(_json.JSONEncoder):
|
|
24
|
+
|
|
25
|
+
def default(self, obj: Any) -> Any:
|
|
26
|
+
rv = None
|
|
27
|
+
if isinstance(obj, complex):
|
|
28
|
+
jsonComplexInstance = JsonComplex(real=obj.real, imag=obj.imag)
|
|
29
|
+
rv = dataclasses.asdict(jsonComplexInstance)
|
|
30
|
+
elif dataclasses.is_dataclass(obj):
|
|
31
|
+
rv = dataclasses.asdict(obj)
|
|
32
|
+
else:
|
|
33
|
+
rv = super().default(obj)
|
|
34
|
+
return rv
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dump(data: Any,
|
|
38
|
+
fo: TextIO,
|
|
39
|
+
*,
|
|
40
|
+
skipkeys=False,
|
|
41
|
+
ensure_ascii=True,
|
|
42
|
+
check_circular=True,
|
|
43
|
+
allow_nan=True,
|
|
44
|
+
indent=None,
|
|
45
|
+
separators=None,
|
|
46
|
+
default=None,
|
|
47
|
+
sort_keys=False,
|
|
48
|
+
**kw):
|
|
49
|
+
rv = _json.dump(data,
|
|
50
|
+
fo,
|
|
51
|
+
skipkeys=skipkeys,
|
|
52
|
+
ensure_ascii=ensure_ascii,
|
|
53
|
+
check_circular=check_circular,
|
|
54
|
+
allow_nan=allow_nan,
|
|
55
|
+
cls=JsonEncoderExtension,
|
|
56
|
+
indent=indent,
|
|
57
|
+
separators=separators,
|
|
58
|
+
default=default,
|
|
59
|
+
sort_keys=sort_keys,
|
|
60
|
+
**kw)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def dumps(data: Any,
|
|
64
|
+
*,
|
|
65
|
+
skipkeys=False,
|
|
66
|
+
ensure_ascii=True,
|
|
67
|
+
check_circular=True,
|
|
68
|
+
allow_nan=True,
|
|
69
|
+
indent=None,
|
|
70
|
+
separators=None,
|
|
71
|
+
default=None,
|
|
72
|
+
sort_keys=False,
|
|
73
|
+
**kw) -> str:
|
|
74
|
+
rv = _json.dumps(data,
|
|
75
|
+
skipkeys=skipkeys,
|
|
76
|
+
ensure_ascii=ensure_ascii,
|
|
77
|
+
check_circular=check_circular,
|
|
78
|
+
allow_nan=allow_nan,
|
|
79
|
+
cls=JsonEncoderExtension,
|
|
80
|
+
indent=indent,
|
|
81
|
+
separators=separators,
|
|
82
|
+
default=default,
|
|
83
|
+
sort_keys=sort_keys,
|
|
84
|
+
**kw)
|
|
85
|
+
return rv
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load(fo: TextIO,
|
|
89
|
+
*,
|
|
90
|
+
cls=None,
|
|
91
|
+
parse_float=None,
|
|
92
|
+
parse_int=None,
|
|
93
|
+
parse_constant=None,
|
|
94
|
+
object_pairs_hook=None,
|
|
95
|
+
**kw) -> Any:
|
|
96
|
+
rv = _json.load(fo,
|
|
97
|
+
cls=cls,
|
|
98
|
+
object_hook=jsonDecoderExtension,
|
|
99
|
+
parse_float=parse_float,
|
|
100
|
+
parse_int=parse_int,
|
|
101
|
+
parse_constant=parse_constant,
|
|
102
|
+
object_pairs_hook=object_pairs_hook,
|
|
103
|
+
**kw)
|
|
104
|
+
return rv
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def loads(data: str,
|
|
108
|
+
*,
|
|
109
|
+
cls=None,
|
|
110
|
+
parse_float=None,
|
|
111
|
+
parse_int=None,
|
|
112
|
+
parse_constant=None,
|
|
113
|
+
object_pairs_hook=None,
|
|
114
|
+
**kw) -> Any:
|
|
115
|
+
rv = _json.loads(data,
|
|
116
|
+
cls=cls,
|
|
117
|
+
object_hook=jsonDecoderExtension,
|
|
118
|
+
parse_float=parse_float,
|
|
119
|
+
parse_int=parse_int,
|
|
120
|
+
parse_constant=parse_constant,
|
|
121
|
+
object_pairs_hook=object_pairs_hook,
|
|
122
|
+
**kw)
|
|
123
|
+
return rv
|
|
@@ -35,6 +35,7 @@ class Logger:
|
|
|
35
35
|
"""
|
|
36
36
|
The `Logger` class handles logging to the main gridappsd server.
|
|
37
37
|
"""
|
|
38
|
+
|
|
38
39
|
def __init__(self, gaps, level=INFO):
|
|
39
40
|
self._gaps = gaps
|
|
40
41
|
self._level = level
|
|
@@ -71,8 +72,9 @@ class Logger:
|
|
|
71
72
|
raise AttributeError(f"Log level must be one of {[x for x in _levelToName.values()]}")
|
|
72
73
|
|
|
73
74
|
if not process_identifier:
|
|
74
|
-
raise AttributeError(
|
|
75
|
-
|
|
75
|
+
raise AttributeError(
|
|
76
|
+
f"Must have GRIDAPPSD_APPLICATION_ID or GRIDAPPSD_SERVICE_ID or GRIDAPPSD_PROCESS_ID "
|
|
77
|
+
"set in os environments.")
|
|
76
78
|
status = self._gaps.get_application_status()
|
|
77
79
|
sim_id = self._gaps.get_simulation_id()
|
|
78
80
|
|
|
@@ -89,13 +91,12 @@ class Logger:
|
|
|
89
91
|
"logLevel": _levelToName[level],
|
|
90
92
|
"storeToDb": True
|
|
91
93
|
}
|
|
92
|
-
|
|
94
|
+
|
|
93
95
|
gridappsd_log_level = os.getenv('GRIDAPPSD_LOG_LEVEL')
|
|
94
96
|
if gridappsd_log_level == None:
|
|
95
97
|
gridappsd_log_level = level
|
|
96
98
|
else:
|
|
97
99
|
gridappsd_log_level = _nameToLevel[gridappsd_log_level]
|
|
98
|
-
|
|
100
|
+
|
|
99
101
|
if level >= gridappsd_log_level:
|
|
100
102
|
self._gaps.send(topic, status_message)
|
|
101
|
-
|
|
@@ -5,9 +5,11 @@ import time
|
|
|
5
5
|
import stomp
|
|
6
6
|
from gridappsd import ApplicationController, GridAPPSD, utils, json_extension as json
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
def main():
|
|
9
10
|
loglevel = logging.INFO
|
|
10
|
-
logging.basicConfig(stream=sys.stdout,
|
|
11
|
+
logging.basicConfig(stream=sys.stdout,
|
|
12
|
+
level=loglevel,
|
|
11
13
|
format="%(asctime)s - %(name)s;%(levelname)s|%(message)s",
|
|
12
14
|
datefmt="%Y-%m-%d %H:%M:%S")
|
|
13
15
|
logging.getLogger('stomp.py').setLevel(logging.ERROR)
|
|
@@ -43,7 +45,7 @@ def main():
|
|
|
43
45
|
if gap is None:
|
|
44
46
|
gap = GridAPPSD()
|
|
45
47
|
|
|
46
|
-
except ConnectionRefusedError:
|
|
48
|
+
except ConnectionRefusedError: # Python 3 specific error code
|
|
47
49
|
_log.debug("Retry in 10 seconds")
|
|
48
50
|
gap = appreg = None
|
|
49
51
|
time.sleep(10)
|
|
@@ -57,6 +59,7 @@ def main():
|
|
|
57
59
|
break
|
|
58
60
|
else:
|
|
59
61
|
if appreg is None:
|
|
62
|
+
|
|
60
63
|
def end_app():
|
|
61
64
|
sys.exit(0)
|
|
62
65
|
|
|
@@ -78,5 +81,3 @@ def main():
|
|
|
78
81
|
gap = None
|
|
79
82
|
|
|
80
83
|
time.sleep(2)
|
|
81
|
-
|
|
82
|
-
|
|
@@ -56,6 +56,7 @@ class SimulationArgs(ConfigBase):
|
|
|
56
56
|
timestep_frequency: str = "1000"
|
|
57
57
|
timestep_increment: str = "1000"
|
|
58
58
|
run_realtime: bool = True
|
|
59
|
+
pause_after_measurements: bool = False
|
|
59
60
|
simulation_name: str = "ieee13nodeckt"
|
|
60
61
|
power_flow_solver_method: str = "NR"
|
|
61
62
|
model_creation_config: ModelCreationConfig = __default_model_creation_config__
|
|
@@ -120,12 +121,14 @@ class Simulation:
|
|
|
120
121
|
add_onmeasurement_callback, add_oncomplete_callback or add_onstart_callback method respectively.
|
|
121
122
|
"""
|
|
122
123
|
|
|
123
|
-
def __init__(self, gapps: 'GridAPPSD',
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
def __init__(self, gapps: 'GridAPPSD', run_config: Union[Dict, SimulationConfig]):
|
|
125
|
+
assert type(gapps).__name__ == 'GridAPPSD', "Must be an instance of GridAPPSD"
|
|
126
|
+
if isinstance(run_config, SimulationConfig):
|
|
127
|
+
self._run_config = run_config.asdict()
|
|
128
|
+
elif isinstance(run_config, dict):
|
|
129
|
+
self._run_config = run_config
|
|
130
|
+
else:
|
|
131
|
+
raise TypeError("run_config must be a dictionary or a SimulationConfig")
|
|
129
132
|
# if isinstance(run_config, SimulationConfig):
|
|
130
133
|
# self._run_config = run_config
|
|
131
134
|
# else:
|
|
@@ -157,8 +160,7 @@ class Simulation:
|
|
|
157
160
|
self._log_count = 0
|
|
158
161
|
self._platform_log_count = 0
|
|
159
162
|
|
|
160
|
-
self._num_timesteps = round(
|
|
161
|
-
float(self._run_config["simulation_config"]["duration"]))
|
|
163
|
+
self._num_timesteps = round(float(self._run_config["simulation_config"]["duration"]))
|
|
162
164
|
|
|
163
165
|
# self._num_timesteps = round(
|
|
164
166
|
# float(self._run_config.simulation_config.duration))
|
|
@@ -171,9 +173,7 @@ class Simulation:
|
|
|
171
173
|
def start_simulation(self, timeout=30):
|
|
172
174
|
""" Start the configured simulation by calling the REQUEST_SIMULATION endpoint.
|
|
173
175
|
"""
|
|
174
|
-
resp = self._gapps.get_response(t.REQUEST_SIMULATION,
|
|
175
|
-
self._run_config,
|
|
176
|
-
timeout=timeout)
|
|
176
|
+
resp = self._gapps.get_response(t.REQUEST_SIMULATION, self._run_config, timeout=timeout)
|
|
177
177
|
|
|
178
178
|
if 'simulationId' not in resp:
|
|
179
179
|
message = "Simulation was not able to run\n" + str(resp)
|
|
@@ -184,10 +184,8 @@ class Simulation:
|
|
|
184
184
|
|
|
185
185
|
# Subscribe to the different components necessary to run and receive
|
|
186
186
|
# simulated measurements and messages.
|
|
187
|
-
self._gapps.subscribe(t.simulation_log_topic(self.simulation_id),
|
|
188
|
-
|
|
189
|
-
self._gapps.subscribe(t.simulation_output_topic(self.simulation_id),
|
|
190
|
-
self.__onmeasurement)
|
|
187
|
+
self._gapps.subscribe(t.simulation_log_topic(self.simulation_id), self.__on_simulation_log)
|
|
188
|
+
self._gapps.subscribe(t.simulation_output_topic(self.simulation_id), self.__onmeasurement)
|
|
191
189
|
self._gapps.subscribe(t.platform_log_topic(), self.__on_platformlog)
|
|
192
190
|
|
|
193
191
|
for p in self.__on_start:
|
|
@@ -197,24 +195,21 @@ class Simulation:
|
|
|
197
195
|
""" Pause simulation"""
|
|
198
196
|
_log.debug("Pausing simulation")
|
|
199
197
|
command = dict(command="pause")
|
|
200
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id),
|
|
201
|
-
json.dumps(command))
|
|
198
|
+
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
202
199
|
self._running_or_paused = True
|
|
203
200
|
|
|
204
201
|
def stop(self):
|
|
205
202
|
""" Stop the simulation"""
|
|
206
203
|
_log.debug("Stopping simulation")
|
|
207
204
|
command = dict(command="stop")
|
|
208
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id),
|
|
209
|
-
json.dumps(command))
|
|
205
|
+
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
210
206
|
self._running_or_paused = True
|
|
211
207
|
|
|
212
208
|
def resume(self):
|
|
213
209
|
""" Resume the simulation"""
|
|
214
210
|
_log.debug("Resuming simulation")
|
|
215
211
|
command = dict(command="resume")
|
|
216
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id),
|
|
217
|
-
json.dumps(command))
|
|
212
|
+
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
218
213
|
self._running_or_paused = True
|
|
219
214
|
|
|
220
215
|
def run_loop(self):
|
|
@@ -242,14 +237,12 @@ class Simulation:
|
|
|
242
237
|
|
|
243
238
|
def resume_pause_at(self, pause_in):
|
|
244
239
|
""" Resume the simulation and have it automatically pause after specified amount of seconds later.
|
|
245
|
-
|
|
240
|
+
|
|
246
241
|
:param pause_in: number of seconds to run before pausing the simulation
|
|
247
242
|
"""
|
|
248
|
-
_log.debug("Resuming simulation. Will pause after {} seconds".format(
|
|
249
|
-
pause_in))
|
|
243
|
+
_log.debug("Resuming simulation. Will pause after {} seconds".format(pause_in))
|
|
250
244
|
command = dict(command="resumePauseAt", input=dict(pauseIn=pause_in))
|
|
251
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id),
|
|
252
|
-
json.dumps(command))
|
|
245
|
+
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
253
246
|
self._running_or_paused = True
|
|
254
247
|
|
|
255
248
|
def add_onmeasurement_callback(self, callback, device_filter=()):
|
|
@@ -271,8 +264,7 @@ class Simulation:
|
|
|
271
264
|
:param device_filter: Future filter of measurements
|
|
272
265
|
:return:
|
|
273
266
|
"""
|
|
274
|
-
self.__filterable_measurement_callback_set.add(
|
|
275
|
-
(callback, device_filter))
|
|
267
|
+
self.__filterable_measurement_callback_set.add((callback, device_filter))
|
|
276
268
|
|
|
277
269
|
def add_onstart_callback(self, callback):
|
|
278
270
|
""" registers a start callback that is called when the simulation is started
|
|
@@ -361,4 +353,4 @@ if __name__ == '__main__':
|
|
|
361
353
|
|
|
362
354
|
print(psc.asjson())
|
|
363
355
|
print(sim.asjson())
|
|
364
|
-
pprint(json.loads(sim.asjson()), indent=2)
|
|
356
|
+
pprint(json.loads(sim.asjson()), indent=2)
|
|
@@ -3,11 +3,10 @@ from gridappsd import topics, json_extension as json
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
class Query:
|
|
8
7
|
""" Class to create and execute request to query timeseries data
|
|
9
8
|
"""
|
|
10
|
-
|
|
9
|
+
|
|
11
10
|
def __init__(self, measurement):
|
|
12
11
|
self.queryMeasurement = measurement
|
|
13
12
|
self.queryFilter = []
|
|
@@ -16,11 +15,11 @@ class Query:
|
|
|
16
15
|
|
|
17
16
|
def select(self, *keys):
|
|
18
17
|
""" Defines what fields should be returned from the query.
|
|
19
|
-
|
|
20
|
-
If this function is not called or is called without argument then
|
|
18
|
+
|
|
19
|
+
If this function is not called or is called without argument then
|
|
21
20
|
all the fields are returned.
|
|
22
|
-
|
|
23
|
-
:param keys: list of fields to be returned
|
|
21
|
+
|
|
22
|
+
:param keys: list of fields to be returned
|
|
24
23
|
"""
|
|
25
24
|
for key in keys:
|
|
26
25
|
self.selectCriteria.append(key)
|
|
@@ -28,78 +27,79 @@ class Query:
|
|
|
28
27
|
|
|
29
28
|
def first(self, n=Optional[int]):
|
|
30
29
|
""" Method to add request to return first or oldest data to the query.
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
When n is specified, query will return first or oldest 'n' rows.
|
|
33
|
-
|
|
34
|
-
:param n:
|
|
32
|
+
|
|
33
|
+
:param n:
|
|
35
34
|
"""
|
|
36
35
|
if n is not None:
|
|
37
|
-
self.first = n
|
|
36
|
+
self.first = n
|
|
38
37
|
return self
|
|
39
38
|
|
|
40
39
|
def last(self, n=Optional[int]):
|
|
41
40
|
""" Method to add request to return last or latest data to the query.
|
|
42
|
-
|
|
41
|
+
|
|
43
42
|
When n is specified, query will return last or latest 'n' rows.
|
|
44
|
-
|
|
45
|
-
:param n:
|
|
43
|
+
|
|
44
|
+
:param n:
|
|
46
45
|
"""
|
|
47
46
|
if n is not None:
|
|
48
|
-
self.last = n
|
|
47
|
+
self.last = n
|
|
49
48
|
return self
|
|
50
|
-
|
|
49
|
+
|
|
51
50
|
def ge(self, value):
|
|
52
51
|
""" Method to add 'value greater than or equal to' filter to a key.
|
|
53
|
-
|
|
54
|
-
:param value:
|
|
52
|
+
|
|
53
|
+
:param value:
|
|
55
54
|
"""
|
|
56
55
|
if self.key is None:
|
|
57
56
|
raise ValueError("Key is not specified. Call where_key first.")
|
|
58
|
-
obj = {"key":self.key,"ge":value}
|
|
57
|
+
obj = {"key": self.key, "ge": value}
|
|
59
58
|
self.queryFilter.append(obj)
|
|
60
59
|
return self
|
|
61
|
-
|
|
60
|
+
|
|
62
61
|
def le(self, value):
|
|
63
62
|
""" Method to add 'value less than or equal to' filter to a key.
|
|
64
|
-
|
|
65
|
-
:param value:
|
|
63
|
+
|
|
64
|
+
:param value:
|
|
66
65
|
"""
|
|
67
66
|
if self.key is None:
|
|
68
67
|
raise ValueError("Key is not specified. Call where_key first.")
|
|
69
|
-
obj = {"key":self.key,"le":value}
|
|
68
|
+
obj = {"key": self.key, "le": value}
|
|
70
69
|
self.queryFilter.append(obj)
|
|
71
70
|
return self
|
|
72
|
-
|
|
71
|
+
|
|
73
72
|
def eq(self, value):
|
|
74
73
|
""" Method to add 'value equal to' filter to a key.
|
|
75
|
-
|
|
76
|
-
:param value:
|
|
74
|
+
|
|
75
|
+
:param value:
|
|
77
76
|
"""
|
|
78
77
|
if self.key is None:
|
|
79
78
|
raise ValueError("Key is not specified. Call where_key first.")
|
|
80
|
-
obj = {"key":self.key,"eq":value}
|
|
79
|
+
obj = {"key": self.key, "eq": value}
|
|
81
80
|
self.queryFilter.append(obj)
|
|
82
81
|
return self
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
def between(self, value1, value2):
|
|
85
84
|
""" Method to add 'value between' value1 and value2 filter to a key.
|
|
86
|
-
|
|
85
|
+
|
|
87
86
|
:param value1: defines 'greater than equal to' filter for key's value
|
|
88
87
|
:param value2: defines 'less than equal to' filter for key's value
|
|
89
88
|
"""
|
|
90
89
|
if self.key is None:
|
|
91
90
|
raise ValueError("Key is not specified. Call where_key first.")
|
|
92
|
-
obj = {"key":self.key,"ge":value1}
|
|
91
|
+
obj = {"key": self.key, "ge": value1}
|
|
93
92
|
self.queryFilter.append(obj)
|
|
94
|
-
obj = {"key":self.key,"le":value2}
|
|
93
|
+
obj = {"key": self.key, "le": value2}
|
|
95
94
|
self.queryFilter.append(obj)
|
|
96
95
|
return self
|
|
97
|
-
|
|
96
|
+
|
|
98
97
|
def where_key(self, key):
|
|
99
98
|
self.key = key
|
|
100
99
|
return self
|
|
101
100
|
|
|
102
101
|
def execute(self, gridappsd_obj):
|
|
103
|
-
del self.key
|
|
104
|
-
response = gridappsd_obj.get_response(topics.REQUEST_TIMESERIES_DATA,
|
|
105
|
-
|
|
102
|
+
del self.key
|
|
103
|
+
response = gridappsd_obj.get_response(topics.REQUEST_TIMESERIES_DATA,
|
|
104
|
+
json.dumps(self.__dict__))
|
|
105
|
+
return response
|
|
@@ -73,6 +73,7 @@ REQUEST_REGISTER_APP = ".".join((PROCESS_PREFIX, "request.app.remote.register"))
|
|
|
73
73
|
REQUEST_APP_START = ".".join((PROCESS_PREFIX, "request.app.start"))
|
|
74
74
|
BASE_APPLICATION_HEARTBEAT = ".".join((BASE_TOPIC_PREFIX, "heartbeat"))
|
|
75
75
|
|
|
76
|
+
|
|
76
77
|
def platform_log_topic():
|
|
77
78
|
""" Utility method for getting the platform.log base topic
|
|
78
79
|
"""
|
|
@@ -175,7 +176,8 @@ def simulation_log_topic(simulation_id):
|
|
|
175
176
|
"""
|
|
176
177
|
return "{}.{}".format(BASE_SIMULATION_LOG_TOPIC, simulation_id)
|
|
177
178
|
|
|
178
|
-
|
|
179
|
+
|
|
180
|
+
def field_message_bus_topic(message_bus_id: str, app_id: str = None, agent_id: str = None):
|
|
179
181
|
""" Utility method for getting the publish/subscribe topic for a specific message bus.
|
|
180
182
|
|
|
181
183
|
:param message_bus_id:
|
|
@@ -185,7 +187,7 @@ def field_message_bus_topic(message_bus_id:str, app_id: str=None, agent_id: str=
|
|
|
185
187
|
"""
|
|
186
188
|
assert message_bus_id, "message_bus_id cannot be empty"
|
|
187
189
|
|
|
188
|
-
return f"{BASE_FIELD_TOPIC}.{message_bus_id}"
|
|
190
|
+
return f"{BASE_FIELD_TOPIC}.{message_bus_id}"
|
|
189
191
|
|
|
190
192
|
|
|
191
193
|
def field_message_bus_app_topic(message_bus_id, app_id=None):
|
|
@@ -198,6 +200,7 @@ def field_message_bus_app_topic(message_bus_id, app_id=None):
|
|
|
198
200
|
assert message_bus_id, "message_bus_id cannot be empty"
|
|
199
201
|
return "{}.{}.{}".format(BASE_FIELD_TOPIC, message_bus_id, app_id)
|
|
200
202
|
|
|
203
|
+
|
|
201
204
|
def field_message_bus_agent_topic(message_bus_id, agent_id=None):
|
|
202
205
|
""" Utility method for getting the publish/subscribe topic for a specific message bus.
|
|
203
206
|
|
|
@@ -208,6 +211,7 @@ def field_message_bus_agent_topic(message_bus_id, agent_id=None):
|
|
|
208
211
|
assert message_bus_id, "message_bus_id cannot be empty"
|
|
209
212
|
return "{}.{}.{}".format(BASE_FIELD_TOPIC, message_bus_id, agent_id)
|
|
210
213
|
|
|
214
|
+
|
|
211
215
|
def field_agent_request_queue(message_bus_id, agent_id):
|
|
212
216
|
""" Utility method for getting the request topic for a specific distributed agent
|
|
213
217
|
|
|
@@ -218,6 +222,7 @@ def field_agent_request_queue(message_bus_id, agent_id):
|
|
|
218
222
|
assert message_bus_id, "message_bus_id cannot be empty"
|
|
219
223
|
return "{}.request.{}.{}".format(BASE_FIELD_QUEUE, message_bus_id, agent_id)
|
|
220
224
|
|
|
225
|
+
|
|
221
226
|
def context_request_queue(message_bus_id):
|
|
222
227
|
""" Utility method for getting the request topic for context manager
|
|
223
228
|
|
|
@@ -225,35 +230,40 @@ def context_request_queue(message_bus_id):
|
|
|
225
230
|
:return:
|
|
226
231
|
"""
|
|
227
232
|
assert message_bus_id, "message_bus_id cannot be empty"
|
|
228
|
-
|
|
229
|
-
return "{}.request.{}.{}".format(BASE_FIELD_QUEUE, message_bus_id,
|
|
233
|
+
|
|
234
|
+
return "{}.request.{}.{}".format(BASE_FIELD_QUEUE, message_bus_id,
|
|
235
|
+
message_bus_id + '.context_manager')
|
|
236
|
+
|
|
230
237
|
|
|
231
238
|
def field_output_topic(message_bus_id=None, simulation_id=None):
|
|
232
239
|
""" Utility method for getting the field output topic.
|
|
233
240
|
If message_bus_id is None, it returns topic used by centralized device interfaces to publish measurements.
|
|
234
|
-
If message_bus_id is not None, it returns topic used by distributed devices interfaces to publish measurements which is then subscribed by distributed agents.
|
|
235
|
-
|
|
241
|
+
If message_bus_id is not None, it returns topic used by distributed devices interfaces to publish measurements which is then subscribed by distributed agents.
|
|
242
|
+
|
|
236
243
|
:param message_bus_id:
|
|
237
244
|
:param simulation_id
|
|
238
|
-
:return: str: Topic to receive field measurements
|
|
245
|
+
:return: str: Topic to receive field measurements
|
|
239
246
|
"""
|
|
240
|
-
|
|
247
|
+
|
|
241
248
|
if simulation_id is None:
|
|
242
249
|
return "{}.{}".format(BASE_FIELD_TOPIC, "output")
|
|
243
250
|
else:
|
|
244
|
-
return "{}.{}.{}.{}".format(BASE_FIELD_TOPIC,"simulation.output",simulation_id,
|
|
245
|
-
|
|
251
|
+
return "{}.{}.{}.{}".format(BASE_FIELD_TOPIC, "simulation.output", simulation_id,
|
|
252
|
+
message_bus_id)
|
|
253
|
+
|
|
254
|
+
|
|
246
255
|
def field_input_topic(message_bus_id=None, simulation_id=None):
|
|
247
256
|
""" Utility method for getting the field input topic.
|
|
248
257
|
If message_bus_id is None, it returns topic used by centralized device interfaces to subscribe to control commands.
|
|
249
|
-
If message_bus_id is not None, it returns topic used by distributed devices interfaces to subscribe to control commands.
|
|
250
|
-
|
|
258
|
+
If message_bus_id is not None, it returns topic used by distributed devices interfaces to subscribe to control commands.
|
|
259
|
+
|
|
251
260
|
:param message_bus_id:
|
|
252
261
|
:param simulation_id
|
|
253
|
-
:return: str: Topic to receive input control commands
|
|
262
|
+
:return: str: Topic to receive input control commands
|
|
254
263
|
"""
|
|
255
|
-
|
|
264
|
+
|
|
256
265
|
if simulation_id is None:
|
|
257
266
|
return "{}.{}".format(BASE_FIELD_TOPIC, "input")
|
|
258
267
|
else:
|
|
259
|
-
return "{}.{}.{}.{}".format(BASE_FIELD_TOPIC,"simulation.input",simulation_id,
|
|
268
|
+
return "{}.{}.{}.{}".format(BASE_FIELD_TOPIC, "simulation.input", simulation_id,
|
|
269
|
+
message_bus_id)
|
|
@@ -4,12 +4,11 @@ from typing import Optional
|
|
|
4
4
|
|
|
5
5
|
from dateutil import parser
|
|
6
6
|
import os
|
|
7
|
-
try:
|
|
7
|
+
try: # python2.7
|
|
8
8
|
from urlparse import urlparse
|
|
9
9
|
except ImportError:
|
|
10
10
|
from urllib.parse import urlparse
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
__GRIDAPPSD_URI__ = os.environ.get("GRIDAPPSD_URI", "localhost:61613")
|
|
14
13
|
|
|
15
14
|
if not __GRIDAPPSD_URI__.startswith("tcp://"):
|
|
@@ -68,8 +67,7 @@ def get_gridappsd_address():
|
|
|
68
67
|
|
|
69
68
|
return address, port
|
|
70
69
|
|
|
71
|
-
return (__GRIDAPPSD_URI_PARSED__.hostname,
|
|
72
|
-
__GRIDAPPSD_URI_PARSED__.port)
|
|
70
|
+
return (__GRIDAPPSD_URI_PARSED__.hostname, __GRIDAPPSD_URI_PARSED__.port)
|
|
73
71
|
|
|
74
72
|
|
|
75
73
|
def get_gridappsd_application_id():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "gridappsd-python"
|
|
3
|
-
version = "2024.
|
|
3
|
+
version = "2024.4.0a0"
|
|
4
4
|
description = "A GridAPPS-D Python Adapter"
|
|
5
5
|
authors = [
|
|
6
6
|
"C. Allwardt <3979063+craig8@users.noreply.github.com>",
|
|
@@ -56,7 +56,7 @@ ignore_patterns = [
|
|
|
56
56
|
"dist/**",
|
|
57
57
|
"docs/**"
|
|
58
58
|
]
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
[tool.yapf]
|
|
61
61
|
based_on_style = "pep8"
|
|
62
62
|
spaces_before_comment = 4
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import json as _json
|
|
3
|
-
from typing import Any, TextIO
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@dataclasses.dataclass
|
|
7
|
-
class JsonComplex:
|
|
8
|
-
real: float
|
|
9
|
-
imag: float
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def jsonDecoderExtension(obj: Any):
|
|
13
|
-
rv = obj
|
|
14
|
-
if isinstance(obj, dict):
|
|
15
|
-
try:
|
|
16
|
-
complexInstance = JsonComplex(**obj)
|
|
17
|
-
rv = complex(complexInstance.real, complexInstance.imag)
|
|
18
|
-
except:
|
|
19
|
-
rv = obj
|
|
20
|
-
return rv
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class JsonEncoderExtension(_json.JSONEncoder):
|
|
24
|
-
def default(self, obj: Any) -> Any:
|
|
25
|
-
rv = None
|
|
26
|
-
if isinstance(obj, complex):
|
|
27
|
-
jsonComplexInstance = JsonComplex(
|
|
28
|
-
real = obj.real,
|
|
29
|
-
imag = obj.imag
|
|
30
|
-
)
|
|
31
|
-
rv = dataclasses.asdict(jsonComplexInstance)
|
|
32
|
-
elif dataclasses.is_dataclass(obj):
|
|
33
|
-
rv = dataclasses.asdict(obj)
|
|
34
|
-
else:
|
|
35
|
-
rv = super().default(obj)
|
|
36
|
-
return rv
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def dump(data: Any, fo: TextIO):
|
|
40
|
-
rv = _json.dump(data, fo, cls=JsonEncoderExtension)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def dumps(data: Any) -> str:
|
|
44
|
-
rv = _json.dumps(data, cls=JsonEncoderExtension)
|
|
45
|
-
return rv
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def load(fo: TextIO) -> Any:
|
|
49
|
-
rv = _json.load(fo, object_hook=jsonDecoderExtension)
|
|
50
|
-
return rv
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def loads(data: str) -> Any:
|
|
54
|
-
rv = _json.loads(data, object_hook=jsonDecoderExtension)
|
|
55
|
-
return rv
|
|
File without changes
|