gridappsd-python 2026.2.1__tar.gz → 2026.2.1a2__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-2026.2.1 → gridappsd_python-2026.2.1a2}/PKG-INFO +1 -1
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/app_registration.py +35 -10
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/goss.py +47 -53
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/gridappsd.py +20 -13
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/simulation.py +41 -23
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/timeseries.py +16 -22
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/pyproject.toml +1 -1
- gridappsd_python-2026.2.1a2/tests/test_app_registration.py +63 -0
- gridappsd_python-2026.2.1a2/tests/test_goss.py +128 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_gridappsd.py +71 -2
- gridappsd_python-2026.2.1a2/tests/test_simulation.py +76 -0
- gridappsd_python-2026.2.1a2/tests/test_simulation_commands.py +93 -0
- gridappsd_python-2026.2.1a2/tests/test_timeseries.py +66 -0
- gridappsd_python-2026.2.1/tests/test_goss.py +0 -204
- gridappsd_python-2026.2.1/tests/test_simulation.py +0 -65
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/.gitignore +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/CHANGELOG.md +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/README.md +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/conf/entrypoint.sh +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/conf/run-gridappsd.sh +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/example.env +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/__init__.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/__main__.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/difference_builder.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/docker_handler.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/field_interface/__init__.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/houses.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/json_extension.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/loghandler.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/register_app.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/topics.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd/utils.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/gridappsd_python_testing.ipynb +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/info/CHANGELOG.md +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/info/VERSION +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/poetry.toml +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/conftest.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/run_gridappsd.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/simulation_fixtures/13_node_2_min_base.json +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_containers.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_difference_builder.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_docker_handler.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_houses.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_json.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_logging.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_logging_integration.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/test_topics.py +0 -0
- {gridappsd_python-2026.2.1 → gridappsd_python-2026.2.1a2}/tests/unittest_test_template.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# import json
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
from enum import Enum
|
|
4
5
|
from queue import Queue
|
|
5
6
|
import time
|
|
6
7
|
import subprocess
|
|
@@ -14,6 +15,30 @@ from . import utils, json_extension as json
|
|
|
14
15
|
|
|
15
16
|
_log = logging.getLogger(__name__)
|
|
16
17
|
|
|
18
|
+
GRIDAPPSD_APPLICATION_STATUS = "GRIDAPPSD_APPLICATION_STATUS"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApplicationStatusEnum(Enum):
|
|
22
|
+
"""Values this module writes to the GRIDAPPSD_APPLICATION_STATUS environment variable.
|
|
23
|
+
|
|
24
|
+
This is a distinct enum from gridappsd.utils.ProcessStatusEnum: this module's
|
|
25
|
+
STOPPED value has no equivalent in ProcessStatusEnum (which has CLOSED
|
|
26
|
+
instead), so reusing that enum here would either drop STOPPED or introduce
|
|
27
|
+
a mismatch between the value written and the value a reader expects.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
STARTING = "STARTING"
|
|
31
|
+
STOPPING = "STOPPING"
|
|
32
|
+
RUNNING = "RUNNING"
|
|
33
|
+
STOPPED = "STOPPED"
|
|
34
|
+
ERROR = "ERROR"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _set_application_status(status: ApplicationStatusEnum) -> None:
|
|
38
|
+
"""Write status to the GRIDAPPSD_APPLICATION_STATUS environment variable."""
|
|
39
|
+
os.environ[GRIDAPPSD_APPLICATION_STATUS] = status.value
|
|
40
|
+
|
|
41
|
+
|
|
17
42
|
# determine OS type
|
|
18
43
|
posix = False
|
|
19
44
|
if os.name == "posix":
|
|
@@ -35,20 +60,20 @@ class Job(threading.Thread):
|
|
|
35
60
|
def run(self):
|
|
36
61
|
try:
|
|
37
62
|
self.running = True
|
|
38
|
-
|
|
63
|
+
_set_application_status(ApplicationStatusEnum.RUNNING)
|
|
39
64
|
|
|
40
65
|
p = subprocess.Popen(args=self._args, shell=False, stdout=self._out, stderr=self._err)
|
|
41
66
|
|
|
42
67
|
# Loop while process is executing
|
|
43
68
|
while p.poll() is None and self.running:
|
|
44
|
-
|
|
69
|
+
_set_application_status(ApplicationStatusEnum.RUNNING)
|
|
45
70
|
time.sleep(1)
|
|
46
71
|
|
|
47
72
|
except Exception as e:
|
|
48
|
-
|
|
73
|
+
_set_application_status(ApplicationStatusEnum.ERROR)
|
|
49
74
|
_log.error(repr(e))
|
|
50
75
|
else:
|
|
51
|
-
|
|
76
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
52
77
|
|
|
53
78
|
|
|
54
79
|
class ApplicationController(object):
|
|
@@ -58,7 +83,7 @@ class ApplicationController(object):
|
|
|
58
83
|
if not isinstance(gridappsd, GridAPPSD):
|
|
59
84
|
raise ValueError("Invalid gridappsd instance passed.")
|
|
60
85
|
|
|
61
|
-
|
|
86
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
62
87
|
self._configDict = config.copy()
|
|
63
88
|
self._validate_config()
|
|
64
89
|
self._gapd = gridappsd
|
|
@@ -80,7 +105,7 @@ class ApplicationController(object):
|
|
|
80
105
|
self._end_callback = None
|
|
81
106
|
self._print_queue = Queue()
|
|
82
107
|
self._heartbeat_thread = None
|
|
83
|
-
|
|
108
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
84
109
|
|
|
85
110
|
if "type" not in self._configDict or self._configDict["type"] != "REMOTE":
|
|
86
111
|
_log.warning(
|
|
@@ -119,7 +144,7 @@ class ApplicationController(object):
|
|
|
119
144
|
self._stop_control_topic = response.get("stopControlTopic")
|
|
120
145
|
|
|
121
146
|
os.environ["GRIDAPPSD_APPLICATION_ID"] = self._application_id
|
|
122
|
-
|
|
147
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
123
148
|
|
|
124
149
|
self._gapd.subscribe(self._stop_control_topic, self.__handle_stop)
|
|
125
150
|
self._gapd.subscribe(self._start_control_topic, self.__handle_start)
|
|
@@ -160,7 +185,7 @@ class ApplicationController(object):
|
|
|
160
185
|
obj = json.loads(message)
|
|
161
186
|
else:
|
|
162
187
|
obj = message
|
|
163
|
-
|
|
188
|
+
_set_application_status(ApplicationStatusEnum.STARTING)
|
|
164
189
|
self._gapd.get_logger().debug("Handling Start: {}\ndict:\n{}".format(headers, obj))
|
|
165
190
|
|
|
166
191
|
if "command" not in obj:
|
|
@@ -176,12 +201,12 @@ class ApplicationController(object):
|
|
|
176
201
|
|
|
177
202
|
def __handle_stop(self, headers, message):
|
|
178
203
|
print("Handling Stop: {} {}".format(headers, message))
|
|
179
|
-
|
|
204
|
+
_set_application_status(ApplicationStatusEnum.STOPPING)
|
|
180
205
|
if self._thread:
|
|
181
206
|
self._thread.join()
|
|
182
207
|
if self._end_callback is not None:
|
|
183
208
|
self._end_callback()
|
|
184
|
-
|
|
209
|
+
_set_application_status(ApplicationStatusEnum.STOPPED)
|
|
185
210
|
|
|
186
211
|
def shutdown(self):
|
|
187
212
|
self._shutting_down = True
|
|
@@ -57,7 +57,6 @@ from enum import Enum
|
|
|
57
57
|
from logging import Logger
|
|
58
58
|
from queue import Queue
|
|
59
59
|
|
|
60
|
-
import stomp as _stomp_module
|
|
61
60
|
from stomp import Connection12 as Connection
|
|
62
61
|
from stomp.exception import NotConnectedException
|
|
63
62
|
from time import sleep
|
|
@@ -66,19 +65,41 @@ from gridappsd import json_extension as json
|
|
|
66
65
|
|
|
67
66
|
_log: Logger = logging.getLogger(inspect.getmodulename(__file__))
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
|
|
69
|
+
def _unpack_stomp_args(*args):
|
|
70
|
+
"""Return (headers, body) from a stomp listener callback's arguments.
|
|
71
|
+
|
|
72
|
+
stomp-py pre 8.x, and this module's own CallbackRouter dispatch, call
|
|
73
|
+
on_message/on_error with two positional arguments: headers and body.
|
|
74
|
+
stomp-py 8.x's raw listener protocol calls with a single Frame object
|
|
75
|
+
exposing .headers and .body instead. A listener reached through
|
|
76
|
+
CallbackRouter (any listener registered via subscribe()) always
|
|
77
|
+
receives the two argument shape, regardless of the installed stomp-py
|
|
78
|
+
version, because CallbackRouter.run_callbacks is the caller, not
|
|
79
|
+
stomp itself. Only a listener registered directly on a raw stomp
|
|
80
|
+
Connection (CallbackRouter itself, TokenResponseListener) is actually
|
|
81
|
+
invoked by stomp's own version dependent dispatch. Detecting the
|
|
82
|
+
argument shape at each call, instead of trusting a single global
|
|
83
|
+
version sniffed flag, is correct for both callers.
|
|
84
|
+
"""
|
|
85
|
+
if len(args) >= 2:
|
|
86
|
+
return args[0], args[1]
|
|
87
|
+
frame = args[0]
|
|
88
|
+
if hasattr(frame, "headers") and hasattr(frame, "body"):
|
|
89
|
+
return frame.headers, frame.body
|
|
90
|
+
headers, body = frame
|
|
91
|
+
return headers, body
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _serialize_message(message):
|
|
95
|
+
"""Return message ready to send on the wire.
|
|
96
|
+
|
|
97
|
+
A list or dict body is serialized to a JSON string; any other body
|
|
98
|
+
(already a string, bytes, etc.) is passed through unchanged.
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(message, (list, dict)):
|
|
101
|
+
return json.dumps(message)
|
|
102
|
+
return message
|
|
82
103
|
|
|
83
104
|
|
|
84
105
|
class GRIDAPPSD_ENV_ENUM(Enum):
|
|
@@ -168,8 +189,7 @@ class GOSS(object):
|
|
|
168
189
|
|
|
169
190
|
def send(self, topic, message):
|
|
170
191
|
self._make_connection()
|
|
171
|
-
|
|
172
|
-
message = json.dumps(message)
|
|
192
|
+
message = _serialize_message(message)
|
|
173
193
|
_log.debug("Sending topic: {} body: {}".format(topic, message))
|
|
174
194
|
self._conn.send(
|
|
175
195
|
body=message, destination=topic, headers={"GOSS_HAS_SUBJECT": True, "GOSS_SUBJECT": self.__token}
|
|
@@ -185,11 +205,8 @@ class GOSS(object):
|
|
|
185
205
|
if "resultFormat" in message:
|
|
186
206
|
self.result_format = message["resultFormat"]
|
|
187
207
|
|
|
188
|
-
# Change message to string if we have a dictionary.
|
|
189
|
-
|
|
190
|
-
message = json.dumps(message)
|
|
191
|
-
elif isinstance(message, list):
|
|
192
|
-
message = json.dumps(message)
|
|
208
|
+
# Change message to string if we have a dictionary or list.
|
|
209
|
+
message = _serialize_message(message)
|
|
193
210
|
|
|
194
211
|
class ResponseListener(object):
|
|
195
212
|
def __init__(self, topic, result_format):
|
|
@@ -198,11 +215,7 @@ class GOSS(object):
|
|
|
198
215
|
self.result_format = result_format
|
|
199
216
|
|
|
200
217
|
def on_message(self, *args):
|
|
201
|
-
|
|
202
|
-
frame = args[0]
|
|
203
|
-
header, message = frame.headers, frame.body
|
|
204
|
-
else:
|
|
205
|
-
header, message = args[0], args[1]
|
|
218
|
+
header, message = _unpack_stomp_args(*args)
|
|
206
219
|
_log.debug("Internal on message is: {} {}".format(header, message))
|
|
207
220
|
try:
|
|
208
221
|
if self.result_format == "JSON":
|
|
@@ -216,11 +229,7 @@ class GOSS(object):
|
|
|
216
229
|
self.response = dict(error="Invalid json returned", header=header, message=message)
|
|
217
230
|
|
|
218
231
|
def on_error(self, *args):
|
|
219
|
-
|
|
220
|
-
frame = args[0]
|
|
221
|
-
headers, message = frame.headers, frame.body
|
|
222
|
-
else:
|
|
223
|
-
headers, message = args[0], args[1]
|
|
232
|
+
headers, message = _unpack_stomp_args(*args)
|
|
224
233
|
_log.error("ERR: {}".format(headers))
|
|
225
234
|
_log.error("OUR ERROR: {}".format(message))
|
|
226
235
|
|
|
@@ -337,21 +346,13 @@ class GOSS(object):
|
|
|
337
346
|
return self.__token
|
|
338
347
|
|
|
339
348
|
def on_message(self, *args):
|
|
340
|
-
|
|
341
|
-
frame = args[0]
|
|
342
|
-
header, message = frame.headers, frame.body
|
|
343
|
-
else:
|
|
344
|
-
header, message = args[0], args[1]
|
|
349
|
+
header, message = _unpack_stomp_args(*args)
|
|
345
350
|
_log.debug("Internal on message is: {} {}".format(header, message))
|
|
346
351
|
|
|
347
352
|
self.__token = str(message)
|
|
348
353
|
|
|
349
354
|
def on_error(self, *args):
|
|
350
|
-
|
|
351
|
-
frame = args[0]
|
|
352
|
-
headers, message = frame.headers, frame.body
|
|
353
|
-
else:
|
|
354
|
-
headers, message = args[0], args[1]
|
|
355
|
+
headers, message = _unpack_stomp_args(*args)
|
|
355
356
|
_log.error("ERR: {}".format(headers))
|
|
356
357
|
_log.error("OUR ERROR: {}".format(message))
|
|
357
358
|
|
|
@@ -404,9 +405,10 @@ class CallbackRouter(object):
|
|
|
404
405
|
cb, hdrs, msg = self._queue_callerback.get()
|
|
405
406
|
try:
|
|
406
407
|
msg = json.loads(msg)
|
|
407
|
-
except:
|
|
408
|
+
except (TypeError, ValueError):
|
|
409
|
+
# msg was not JSON text (already a dict, or plain string body);
|
|
410
|
+
# pass it through unchanged rather than as a decode failure.
|
|
408
411
|
pass
|
|
409
|
-
# msg = message
|
|
410
412
|
|
|
411
413
|
for c in cb:
|
|
412
414
|
c(hdrs, msg)
|
|
@@ -429,11 +431,7 @@ class CallbackRouter(object):
|
|
|
429
431
|
pass
|
|
430
432
|
|
|
431
433
|
def on_message(self, *args):
|
|
432
|
-
|
|
433
|
-
frame = args[0]
|
|
434
|
-
headers, message = frame.headers, frame.body
|
|
435
|
-
else:
|
|
436
|
-
headers, message = args[0], args[1]
|
|
434
|
+
headers, message = _unpack_stomp_args(*args)
|
|
437
435
|
destination = headers["destination"]
|
|
438
436
|
# _log.debug("Topic map keys are: {keys}".format(keys=self._topics_callback_map.keys()))
|
|
439
437
|
if destination in self._topics_callback_map:
|
|
@@ -442,11 +440,7 @@ class CallbackRouter(object):
|
|
|
442
440
|
_log.error("INVALID DESTINATION {destination}".format(destination=destination))
|
|
443
441
|
|
|
444
442
|
def on_error(self, *args):
|
|
445
|
-
|
|
446
|
-
frame = args[0]
|
|
447
|
-
header, message = frame.headers, frame.body
|
|
448
|
-
else:
|
|
449
|
-
header, message = args[0], args[1]
|
|
443
|
+
header, message = _unpack_stomp_args(*args)
|
|
450
444
|
_log.error("Error in callback router")
|
|
451
445
|
_log.error(header)
|
|
452
446
|
_log.error(message)
|
|
@@ -116,29 +116,32 @@ class GridAPPSD(GOSS):
|
|
|
116
116
|
self._simulation_log_topic = t.simulation_log_topic(self._simulation_id)
|
|
117
117
|
return self._simulation_id
|
|
118
118
|
|
|
119
|
-
def
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
:param status:
|
|
119
|
+
def _set_status(self, status, kind):
|
|
120
|
+
"""Set self._process_status, warning instead of raising on an invalid value.
|
|
121
|
+
|
|
122
|
+
:param status: candidate value for ProcessStatusEnum
|
|
123
|
+
:param kind: human readable label for the warning message, e.g. "application"
|
|
123
124
|
"""
|
|
124
125
|
try:
|
|
125
126
|
self._process_status = ProcessStatusEnum(status)
|
|
126
127
|
except ValueError:
|
|
127
128
|
self.get_logger().warning(
|
|
128
|
-
"Unsuccessful change of
|
|
129
|
+
f"Unsuccessful change of {kind} status." + f"Valid statuses are {ProcessStatusEnum.__members__}."
|
|
129
130
|
)
|
|
130
131
|
|
|
132
|
+
def set_application_status(self, status):
|
|
133
|
+
"""
|
|
134
|
+
Set the application status.
|
|
135
|
+
:param status:
|
|
136
|
+
"""
|
|
137
|
+
self._set_status(status, "application")
|
|
138
|
+
|
|
131
139
|
def set_service_status(self, status):
|
|
132
140
|
"""
|
|
133
141
|
Set the service status.
|
|
134
142
|
:param status:
|
|
135
143
|
"""
|
|
136
|
-
|
|
137
|
-
self._process_status = ProcessStatusEnum(status)
|
|
138
|
-
except ValueError:
|
|
139
|
-
self.get_logger().warning(
|
|
140
|
-
"Unsuccessful change of service status." + f"Valid statuses are {ProcessStatusEnum.__members__}."
|
|
141
|
-
)
|
|
144
|
+
self._set_status(status, "service")
|
|
142
145
|
|
|
143
146
|
def set_simulation_id(self, simulation_id):
|
|
144
147
|
if simulation_id is None:
|
|
@@ -148,12 +151,16 @@ class GridAPPSD(GOSS):
|
|
|
148
151
|
self._simulation_id = simulation_id
|
|
149
152
|
self._simulation_log_topic = t.simulation_log_topic(self._simulation_id)
|
|
150
153
|
|
|
154
|
+
def _get_status(self):
|
|
155
|
+
"""Return self._process_status as its plain string value."""
|
|
156
|
+
return self._process_status.value
|
|
157
|
+
|
|
151
158
|
def get_application_status(self):
|
|
152
159
|
"""
|
|
153
160
|
Return the application status
|
|
154
161
|
:return:
|
|
155
162
|
"""
|
|
156
|
-
return self.
|
|
163
|
+
return self._get_status()
|
|
157
164
|
|
|
158
165
|
def get_application_id(self):
|
|
159
166
|
return utils.get_gridappsd_application_id()
|
|
@@ -163,7 +170,7 @@ class GridAPPSD(GOSS):
|
|
|
163
170
|
Return the service status
|
|
164
171
|
:return:
|
|
165
172
|
"""
|
|
166
|
-
return self.
|
|
173
|
+
return self._get_status()
|
|
167
174
|
|
|
168
175
|
def query_object_types(self, model_id=None):
|
|
169
176
|
"""Allows the caller to query the different object types.
|
|
@@ -42,11 +42,11 @@ class ConfigBase:
|
|
|
42
42
|
|
|
43
43
|
@dataclass
|
|
44
44
|
class ModelCreationConfig(ConfigBase):
|
|
45
|
-
load_scaling_factor:
|
|
45
|
+
load_scaling_factor: float = field(default=1)
|
|
46
46
|
schedule_name: str = field(default="ieeezipload")
|
|
47
|
-
z_fraction:
|
|
48
|
-
i_fraction:
|
|
49
|
-
p_fraction:
|
|
47
|
+
z_fraction: float = field(default=0)
|
|
48
|
+
i_fraction: float = field(default=1)
|
|
49
|
+
p_fraction: float = field(default=0)
|
|
50
50
|
randomize_zipload_fractions: bool = field(default=False)
|
|
51
51
|
use_houses: bool = field(default=False)
|
|
52
52
|
|
|
@@ -56,14 +56,30 @@ class ModelCreationConfig(ConfigBase):
|
|
|
56
56
|
|
|
57
57
|
@dataclass
|
|
58
58
|
class SimulationArgs(ConfigBase):
|
|
59
|
-
start_time:
|
|
60
|
-
duration:
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
start_time: int = field(default=1655321830)
|
|
60
|
+
duration: int = field(default=300)
|
|
61
|
+
publish_period: int | None = field(default=None)
|
|
62
|
+
interval: int | None = field(default=None)
|
|
63
63
|
run_realtime: bool = field(default=True)
|
|
64
64
|
pause_after_measurements: bool = field(default=False)
|
|
65
65
|
simulation_name: str = field(default="ieee13nodeckt")
|
|
66
66
|
|
|
67
|
+
def __post_init_(self):
|
|
68
|
+
if self.run_realtime:
|
|
69
|
+
self.interval = 1
|
|
70
|
+
if not self.publish_period:
|
|
71
|
+
self.publish_period = 3
|
|
72
|
+
else:
|
|
73
|
+
if not self.interval:
|
|
74
|
+
self.interval = 60
|
|
75
|
+
if not self.publish_period:
|
|
76
|
+
self.publish_period = 60
|
|
77
|
+
if self.publish_period < self.interval:
|
|
78
|
+
raise RuntimeError(
|
|
79
|
+
"A simulation's publishing_period cannot be less than the simulation's timestep "
|
|
80
|
+
"interval. please make the publishing_period >= interval!"
|
|
81
|
+
)
|
|
82
|
+
|
|
67
83
|
|
|
68
84
|
@dataclass
|
|
69
85
|
class SimulatorArgs(ConfigBase):
|
|
@@ -201,26 +217,30 @@ class Simulation:
|
|
|
201
217
|
for p in self.__on_start:
|
|
202
218
|
p(self)
|
|
203
219
|
|
|
204
|
-
def
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
command
|
|
220
|
+
def _send_simulation_command(self, command_name, **command_input):
|
|
221
|
+
"""Send a command to this simulation's input topic.
|
|
222
|
+
|
|
223
|
+
:param command_name: value of the command field sent to the simulation
|
|
224
|
+
:param command_input: when given, nested under the command's "input" key
|
|
225
|
+
"""
|
|
226
|
+
_log.debug("Sending simulation command: {}".format(command_name))
|
|
227
|
+
command = dict(command=command_name)
|
|
228
|
+
if command_input:
|
|
229
|
+
command["input"] = command_input
|
|
208
230
|
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
209
231
|
self._running_or_paused = True
|
|
210
232
|
|
|
233
|
+
def pause(self):
|
|
234
|
+
"""Pause simulation"""
|
|
235
|
+
self._send_simulation_command("pause")
|
|
236
|
+
|
|
211
237
|
def stop(self):
|
|
212
238
|
"""Stop the simulation"""
|
|
213
|
-
|
|
214
|
-
command = dict(command="stop")
|
|
215
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
216
|
-
self._running_or_paused = True
|
|
239
|
+
self._send_simulation_command("stop")
|
|
217
240
|
|
|
218
241
|
def resume(self):
|
|
219
242
|
"""Resume the simulation"""
|
|
220
|
-
|
|
221
|
-
command = dict(command="resume")
|
|
222
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
223
|
-
self._running_or_paused = True
|
|
243
|
+
self._send_simulation_command("resume")
|
|
224
244
|
|
|
225
245
|
def run_loop(self):
|
|
226
246
|
"""Loop around the running of the simulation itself.
|
|
@@ -251,9 +271,7 @@ class Simulation:
|
|
|
251
271
|
:param pause_in: number of seconds to run before pausing the simulation
|
|
252
272
|
"""
|
|
253
273
|
_log.debug("Resuming simulation. Will pause after {} seconds".format(pause_in))
|
|
254
|
-
|
|
255
|
-
self._gapps.send(t.simulation_input_topic(self.simulation_id), json.dumps(command))
|
|
256
|
-
self._running_or_paused = True
|
|
274
|
+
self._send_simulation_command("resumePauseAt", pauseIn=pause_in)
|
|
257
275
|
|
|
258
276
|
def add_onmeasurement_callback(self, callback, device_filter=()):
|
|
259
277
|
"""registers an onmeasurment callback to be called when measurements have come through.
|
|
@@ -46,38 +46,37 @@ class Query:
|
|
|
46
46
|
self.last = n
|
|
47
47
|
return self
|
|
48
48
|
|
|
49
|
-
def
|
|
50
|
-
"""
|
|
49
|
+
def _add_filter(self, operator, value):
|
|
50
|
+
"""Append a single {key, operator: value} filter for the current key.
|
|
51
51
|
|
|
52
|
-
:param
|
|
52
|
+
:param operator: one of "ge", "le", "eq"
|
|
53
|
+
:param value: value to filter on
|
|
53
54
|
"""
|
|
54
55
|
if self.key is None:
|
|
55
56
|
raise ValueError("Key is not specified. Call where_key first.")
|
|
56
|
-
|
|
57
|
-
self.queryFilter.append(obj)
|
|
57
|
+
self.queryFilter.append({"key": self.key, operator: value})
|
|
58
58
|
return self
|
|
59
59
|
|
|
60
|
+
def ge(self, value):
|
|
61
|
+
"""Method to add 'value greater than or equal to' filter to a key.
|
|
62
|
+
|
|
63
|
+
:param value:
|
|
64
|
+
"""
|
|
65
|
+
return self._add_filter("ge", value)
|
|
66
|
+
|
|
60
67
|
def le(self, value):
|
|
61
68
|
"""Method to add 'value less than or equal to' filter to a key.
|
|
62
69
|
|
|
63
70
|
:param value:
|
|
64
71
|
"""
|
|
65
|
-
|
|
66
|
-
raise ValueError("Key is not specified. Call where_key first.")
|
|
67
|
-
obj = {"key": self.key, "le": value}
|
|
68
|
-
self.queryFilter.append(obj)
|
|
69
|
-
return self
|
|
72
|
+
return self._add_filter("le", value)
|
|
70
73
|
|
|
71
74
|
def eq(self, value):
|
|
72
75
|
"""Method to add 'value equal to' filter to a key.
|
|
73
76
|
|
|
74
77
|
:param value:
|
|
75
78
|
"""
|
|
76
|
-
|
|
77
|
-
raise ValueError("Key is not specified. Call where_key first.")
|
|
78
|
-
obj = {"key": self.key, "eq": value}
|
|
79
|
-
self.queryFilter.append(obj)
|
|
80
|
-
return self
|
|
79
|
+
return self._add_filter("eq", value)
|
|
81
80
|
|
|
82
81
|
def between(self, value1, value2):
|
|
83
82
|
"""Method to add 'value between' value1 and value2 filter to a key.
|
|
@@ -85,13 +84,8 @@ class Query:
|
|
|
85
84
|
:param value1: defines 'greater than equal to' filter for key's value
|
|
86
85
|
:param value2: defines 'less than equal to' filter for key's value
|
|
87
86
|
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
obj = {"key": self.key, "ge": value1}
|
|
91
|
-
self.queryFilter.append(obj)
|
|
92
|
-
obj = {"key": self.key, "le": value2}
|
|
93
|
-
self.queryFilter.append(obj)
|
|
94
|
-
return self
|
|
87
|
+
self._add_filter("ge", value1)
|
|
88
|
+
return self._add_filter("le", value2)
|
|
95
89
|
|
|
96
90
|
def where_key(self, key):
|
|
97
91
|
self.key = key
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Unit tests for gridappsd.app_registration status handling.
|
|
2
|
+
|
|
3
|
+
_set_application_status writes ApplicationStatusEnum values to the
|
|
4
|
+
GRIDAPPSD_APPLICATION_STATUS environment variable. These tests assert
|
|
5
|
+
the exact string value written for each status, and that Job.run()
|
|
6
|
+
drives the environment variable through the expected RUNNING then
|
|
7
|
+
STOPPED transition for a command that exits cleanly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from gridappsd.app_registration import (
|
|
15
|
+
GRIDAPPSD_APPLICATION_STATUS,
|
|
16
|
+
ApplicationStatusEnum,
|
|
17
|
+
Job,
|
|
18
|
+
_set_application_status,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture(autouse=True)
|
|
23
|
+
def _clear_status_env(monkeypatch):
|
|
24
|
+
monkeypatch.delenv(GRIDAPPSD_APPLICATION_STATUS, raising=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestSetApplicationStatus:
|
|
28
|
+
@pytest.mark.parametrize(
|
|
29
|
+
"status",
|
|
30
|
+
[
|
|
31
|
+
ApplicationStatusEnum.STARTING,
|
|
32
|
+
ApplicationStatusEnum.STOPPING,
|
|
33
|
+
ApplicationStatusEnum.RUNNING,
|
|
34
|
+
ApplicationStatusEnum.STOPPED,
|
|
35
|
+
ApplicationStatusEnum.ERROR,
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
def test_writes_exact_status_value_to_environment(self, status):
|
|
39
|
+
_set_application_status(status)
|
|
40
|
+
|
|
41
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == status.value
|
|
42
|
+
|
|
43
|
+
def test_overwrites_previous_value(self):
|
|
44
|
+
_set_application_status(ApplicationStatusEnum.STARTING)
|
|
45
|
+
_set_application_status(ApplicationStatusEnum.RUNNING)
|
|
46
|
+
|
|
47
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == "RUNNING"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestJobRunSetsApplicationStatus:
|
|
51
|
+
def test_successful_job_ends_in_stopped_status(self):
|
|
52
|
+
job = Job(args=["true"])
|
|
53
|
+
|
|
54
|
+
job.run()
|
|
55
|
+
|
|
56
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == ApplicationStatusEnum.STOPPED.value
|
|
57
|
+
|
|
58
|
+
def test_failing_job_ends_in_error_status(self):
|
|
59
|
+
job = Job(args=["/nonexistent-command-for-gadp-044-test"])
|
|
60
|
+
|
|
61
|
+
job.run()
|
|
62
|
+
|
|
63
|
+
assert os.environ[GRIDAPPSD_APPLICATION_STATUS] == ApplicationStatusEnum.ERROR.value
|