genie-python 15.1.0rc1__py3-none-any.whl
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.
- genie_python/.pylintrc +539 -0
- genie_python/__init__.py +1 -0
- genie_python/block_names.py +123 -0
- genie_python/channel_access_exceptions.py +45 -0
- genie_python/genie.py +2462 -0
- genie_python/genie_advanced.py +418 -0
- genie_python/genie_alerts.py +195 -0
- genie_python/genie_api_setup.py +451 -0
- genie_python/genie_blockserver.py +64 -0
- genie_python/genie_cachannel_wrapper.py +545 -0
- genie_python/genie_change_cache.py +151 -0
- genie_python/genie_dae.py +2218 -0
- genie_python/genie_epics_api.py +906 -0
- genie_python/genie_experimental_data.py +186 -0
- genie_python/genie_logging.py +200 -0
- genie_python/genie_p4p_wrapper.py +203 -0
- genie_python/genie_plot.py +77 -0
- genie_python/genie_pre_post_cmd_manager.py +21 -0
- genie_python/genie_pv_connection_protocol.py +36 -0
- genie_python/genie_script_checker.py +507 -0
- genie_python/genie_script_generator.py +212 -0
- genie_python/genie_simulate.py +69 -0
- genie_python/genie_simulate_impl.py +1265 -0
- genie_python/genie_startup.py +29 -0
- genie_python/genie_toggle_settings.py +58 -0
- genie_python/genie_wait_for_move.py +154 -0
- genie_python/genie_waitfor.py +576 -0
- genie_python/matplotlib_backend/__init__.py +0 -0
- genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
- genie_python/mysql_abstraction_layer.py +272 -0
- genie_python/run_tests.py +56 -0
- genie_python/scanning_instrument_pylint_plugin.py +31 -0
- genie_python/typings/CaChannel/CaChannel.pyi +893 -0
- genie_python/typings/CaChannel/__init__.pyi +9 -0
- genie_python/typings/CaChannel/_version.pyi +6 -0
- genie_python/typings/CaChannel/ca.pyi +31 -0
- genie_python/utilities.py +406 -0
- genie_python/version.py +1 -0
- genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
- genie_python-15.1.0rc1.dist-info/METADATA +95 -0
- genie_python-15.1.0rc1.dist-info/RECORD +43 -0
- genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
- genie_python-15.1.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
from __future__ import absolute_import, print_function
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import urllib.request
|
|
9
|
+
from builtins import str
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from io import open
|
|
12
|
+
from typing import TYPE_CHECKING, Callable
|
|
13
|
+
|
|
14
|
+
from genie_python.block_names import BlockNames, BlockNamesManager
|
|
15
|
+
from genie_python.channel_access_exceptions import UnableToConnectToPVException
|
|
16
|
+
from genie_python.genie_blockserver import BlockServer
|
|
17
|
+
from genie_python.genie_cachannel_wrapper import CaChannelWrapper as Wrapper
|
|
18
|
+
from genie_python.genie_dae import Dae
|
|
19
|
+
from genie_python.genie_experimental_data import GetExperimentData
|
|
20
|
+
from genie_python.genie_logging import GenieLogger
|
|
21
|
+
from genie_python.genie_logging import filter as logging_filter
|
|
22
|
+
from genie_python.genie_pre_post_cmd_manager import PrePostCmdManager
|
|
23
|
+
from genie_python.genie_wait_for_move import WaitForMoveController
|
|
24
|
+
from genie_python.genie_waitfor import WaitForController
|
|
25
|
+
from genie_python.utilities import (
|
|
26
|
+
EnvironmentDetails,
|
|
27
|
+
crc8,
|
|
28
|
+
dehex_decompress_and_dejson,
|
|
29
|
+
remove_field_from_pv,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from genie_python.genie import PVValue
|
|
34
|
+
|
|
35
|
+
RC_ENABLE = ":RC:ENABLE"
|
|
36
|
+
RC_LOW = ":RC:LOW"
|
|
37
|
+
RC_HIGH = ":RC:HIGH"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Block names and its manager which automatically gets populated
|
|
41
|
+
# with the names of the current blocks
|
|
42
|
+
BLOCK_NAMES = BlockNames()
|
|
43
|
+
BLOCK_NAMES_MANAGER = BlockNamesManager(BLOCK_NAMES)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class API(object):
|
|
47
|
+
def __init__(
|
|
48
|
+
self, pv_prefix: str, globs: dict, environment_details: EnvironmentDetails | None = None
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Constructor for the EPICS enabled API.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
pv_prefix: used for prefixing the PV and block names
|
|
55
|
+
globs: globals
|
|
56
|
+
environment_details: details of the computer environment
|
|
57
|
+
"""
|
|
58
|
+
self.waitfor = None # type: WaitForController
|
|
59
|
+
self.wait_for_move = None
|
|
60
|
+
self.dae = None # type: Dae
|
|
61
|
+
self.blockserver = None # type: BlockServer
|
|
62
|
+
self.exp_data = None # type: GetExperimentData
|
|
63
|
+
self.inst_prefix = ""
|
|
64
|
+
self.instrument_name = ""
|
|
65
|
+
self.machine_name = ""
|
|
66
|
+
self.localmod = None
|
|
67
|
+
self.block_prefix = "CS:SB:"
|
|
68
|
+
self.motion_suffix = "CS:MOT:MOVING"
|
|
69
|
+
self.pre_post_cmd_manager = PrePostCmdManager()
|
|
70
|
+
self.logger = GenieLogger()
|
|
71
|
+
|
|
72
|
+
if environment_details is None:
|
|
73
|
+
self._environment_details = EnvironmentDetails()
|
|
74
|
+
else:
|
|
75
|
+
self._environment_details = environment_details
|
|
76
|
+
|
|
77
|
+
Wrapper.errorLogFunc = self.logger.log_ca_msg
|
|
78
|
+
|
|
79
|
+
# disable CA error messages to console from disconnected PVs
|
|
80
|
+
import ctypes
|
|
81
|
+
|
|
82
|
+
if os.name == "nt":
|
|
83
|
+
comdll = "COM.DLL"
|
|
84
|
+
else:
|
|
85
|
+
comdll = "libCom.so"
|
|
86
|
+
try:
|
|
87
|
+
hcom = ctypes.cdll.LoadLibrary(comdll)
|
|
88
|
+
hcom.eltc(ctypes.c_int(0))
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print("Unable to disable CA console errors from {}: {}".format(comdll, e))
|
|
91
|
+
|
|
92
|
+
def get_instrument(self) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Gets the name of the local instrument (e.g. NDW1234, DEMO, EMMA-A)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
the name of the local instrument
|
|
98
|
+
"""
|
|
99
|
+
return self.instrument_name
|
|
100
|
+
|
|
101
|
+
def get_instrument_py_name(self) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Gets the name of the local instrument in lowercase and with "-" replaced with "_"
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
the name of the local instrument in a python-friendly format
|
|
107
|
+
"""
|
|
108
|
+
return self.instrument_name.lower().replace("-", "_")
|
|
109
|
+
|
|
110
|
+
def _get_machine_details_from_identifier(self, machine_identifier: str) -> tuple[str, str, str]:
|
|
111
|
+
"""
|
|
112
|
+
Gets the details of a machine by looking it up in the instrument list first.
|
|
113
|
+
If there is no match it calculates the details as usual.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
machine_identifier: should be the pv prefix but also accepts instrument name;
|
|
117
|
+
if none defaults to computer host name
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The instrument name, machine name and pv_prefix based in the machine identifier
|
|
121
|
+
"""
|
|
122
|
+
instrument_pv_prefix = "IN:"
|
|
123
|
+
test_machine_pv_prefix = "TE:"
|
|
124
|
+
|
|
125
|
+
instrument_machine_prefixes = ["NDX", "NDE"]
|
|
126
|
+
test_machine_prefixes = ["NDH"]
|
|
127
|
+
|
|
128
|
+
if machine_identifier is None:
|
|
129
|
+
machine_identifier = self._environment_details.get_host_name()
|
|
130
|
+
|
|
131
|
+
# machine_identifier needs to be uppercase for both 'NDXALF' and 'ndxalf' to be valid
|
|
132
|
+
machine_identifier = machine_identifier.upper()
|
|
133
|
+
|
|
134
|
+
# get the dehexed, decompressed list of instruments from the PV INSTLIST
|
|
135
|
+
# then find the first match where pvPrefix equals the machine identifier
|
|
136
|
+
# that's been passed to this function if it is not found instrument_details will be None
|
|
137
|
+
instrument_details = None
|
|
138
|
+
try:
|
|
139
|
+
instrument_list = dehex_decompress_and_dejson(self.get_pv_value("CS:INSTLIST"))
|
|
140
|
+
instrument_details = next(
|
|
141
|
+
(inst for inst in instrument_list if inst["pvPrefix"] == machine_identifier), None
|
|
142
|
+
)
|
|
143
|
+
except UnableToConnectToPVException as error:
|
|
144
|
+
print(
|
|
145
|
+
"An exception occured while loading genie python:",
|
|
146
|
+
error,
|
|
147
|
+
"\nContinuing execution...",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if instrument_details is not None:
|
|
151
|
+
instrument = instrument_details["name"]
|
|
152
|
+
else:
|
|
153
|
+
instrument = machine_identifier.upper()
|
|
154
|
+
for p in (
|
|
155
|
+
[instrument_pv_prefix, test_machine_pv_prefix]
|
|
156
|
+
+ instrument_machine_prefixes
|
|
157
|
+
+ test_machine_prefixes
|
|
158
|
+
):
|
|
159
|
+
if machine_identifier.startswith(p):
|
|
160
|
+
instrument = machine_identifier.upper()[len(p) :].rstrip(":")
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
if instrument_details is not None:
|
|
164
|
+
machine = instrument_details["hostName"]
|
|
165
|
+
elif machine_identifier.startswith(instrument_pv_prefix):
|
|
166
|
+
machine = "NDX{0}".format(instrument)
|
|
167
|
+
elif machine_identifier.startswith(test_machine_pv_prefix):
|
|
168
|
+
machine = instrument
|
|
169
|
+
else:
|
|
170
|
+
machine = machine_identifier.upper()
|
|
171
|
+
|
|
172
|
+
is_instrument = any(
|
|
173
|
+
machine_identifier.startswith(p)
|
|
174
|
+
for p in instrument_machine_prefixes + [instrument_pv_prefix]
|
|
175
|
+
)
|
|
176
|
+
pv_prefix = self._get_pv_prefix(instrument, is_instrument)
|
|
177
|
+
|
|
178
|
+
return instrument, machine, pv_prefix
|
|
179
|
+
|
|
180
|
+
def get_instrument_full_name(self) -> str:
|
|
181
|
+
return self.machine_name
|
|
182
|
+
|
|
183
|
+
def set_instrument(
|
|
184
|
+
self, machine_identifier: str, globs: dict, import_instrument_init: bool = True
|
|
185
|
+
) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Set the instrument being used by setting the PV prefix or by the
|
|
188
|
+
hostname if no prefix was passed.
|
|
189
|
+
|
|
190
|
+
Will do some checking to allow you to pass instrument names in so.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
machine_identifier: should be the pv prefix but also accepts
|
|
194
|
+
instrument name; if none defaults to computer host name
|
|
195
|
+
globs: globals
|
|
196
|
+
import_instrument_init (bool): if True import the instrument init
|
|
197
|
+
from the config area; otherwise don't
|
|
198
|
+
"""
|
|
199
|
+
instrument, machine, pv_prefix = self._get_machine_details_from_identifier(
|
|
200
|
+
machine_identifier
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
print("PV prefix is " + pv_prefix)
|
|
204
|
+
self.inst_prefix = pv_prefix
|
|
205
|
+
self.instrument_name = instrument
|
|
206
|
+
self.machine_name = machine
|
|
207
|
+
self.dae = Dae(self, pv_prefix)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
self.exp_data = GetExperimentData(machine)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
error_message = (
|
|
213
|
+
"Could not connect to database, RB numbers will not be accessible: {}".format(e)
|
|
214
|
+
)
|
|
215
|
+
self.logger.log_error_msg(error_message)
|
|
216
|
+
print(error_message)
|
|
217
|
+
self.exp_data = None
|
|
218
|
+
|
|
219
|
+
self.wait_for_move = WaitForMoveController(self, pv_prefix + self.motion_suffix)
|
|
220
|
+
self.waitfor = WaitForController(self)
|
|
221
|
+
self.blockserver = BlockServer(self)
|
|
222
|
+
BLOCK_NAMES_MANAGER.update_prefix(pv_prefix)
|
|
223
|
+
|
|
224
|
+
# Set instrument for logging purposes
|
|
225
|
+
logging_filter.instrument = instrument
|
|
226
|
+
|
|
227
|
+
# Whatever machine we're on, try to initialize and fall back if unsuccessful
|
|
228
|
+
self.init_instrument(instrument, machine, globs, import_instrument_init)
|
|
229
|
+
|
|
230
|
+
def _get_pv_prefix(self, instrument: str, is_instrument: bool) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Create the pv prefix based on instrument name and whether it is an
|
|
233
|
+
instrument or a dev machine
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
instrument: instrument name
|
|
237
|
+
is_instrument: True is an instrument; False not an instrument
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
string: the PV prefix
|
|
241
|
+
"""
|
|
242
|
+
clean_instrument = instrument
|
|
243
|
+
if clean_instrument.endswith(":"):
|
|
244
|
+
clean_instrument = clean_instrument[:-1]
|
|
245
|
+
if len(clean_instrument) > 8:
|
|
246
|
+
clean_instrument = clean_instrument[0:6] + crc8(clean_instrument)
|
|
247
|
+
|
|
248
|
+
self.instrument_name = clean_instrument
|
|
249
|
+
|
|
250
|
+
if is_instrument:
|
|
251
|
+
pv_prefix_prefix = "IN"
|
|
252
|
+
print("THIS IS %s!" % self.instrument_name.upper())
|
|
253
|
+
else:
|
|
254
|
+
pv_prefix_prefix = "TE"
|
|
255
|
+
print("THIS IS %s! (test machine)" % self.instrument_name.upper())
|
|
256
|
+
return "{prefix}:{instrument}:".format(
|
|
257
|
+
prefix=pv_prefix_prefix, instrument=self.instrument_name
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def prefix_pv_name(self, name: str) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Adds the instrument prefix to the specified PV.
|
|
263
|
+
"""
|
|
264
|
+
if self.inst_prefix is not None:
|
|
265
|
+
return self.inst_prefix + name
|
|
266
|
+
return name
|
|
267
|
+
|
|
268
|
+
def init_instrument(
|
|
269
|
+
self, instrument: str, machine_name: str, globs: dict, import_instrument_init: bool
|
|
270
|
+
) -> None:
|
|
271
|
+
"""
|
|
272
|
+
Initialise an instrument using the default init file followed by the machine specific init.
|
|
273
|
+
Args:
|
|
274
|
+
instrument: instrument name to load from
|
|
275
|
+
machine_name: machine name
|
|
276
|
+
globs: current globals
|
|
277
|
+
import_instrument_init: if True import the instrument init from the config area;
|
|
278
|
+
otherwise don't
|
|
279
|
+
"""
|
|
280
|
+
if import_instrument_init:
|
|
281
|
+
instrument = instrument.lower().replace("-", "_")
|
|
282
|
+
python_config_area = os.path.join(
|
|
283
|
+
"C:" + os.sep, "Instrument", "Settings", "config", machine_name, "Python"
|
|
284
|
+
)
|
|
285
|
+
print(
|
|
286
|
+
"Loading instrument scripts from: {}".format(
|
|
287
|
+
os.path.join(python_config_area, "inst")
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Check instrument specific folder exists, if so add to sys path
|
|
292
|
+
if os.path.isdir(python_config_area):
|
|
293
|
+
sys.path.append(python_config_area)
|
|
294
|
+
|
|
295
|
+
import importlib
|
|
296
|
+
|
|
297
|
+
# Load the instrument init file
|
|
298
|
+
self.localmod = importlib.import_module("init_{}".format(instrument))
|
|
299
|
+
|
|
300
|
+
if self.localmod.__file__.endswith(".pyc"):
|
|
301
|
+
file_loc = self.localmod.__file__[:-1]
|
|
302
|
+
else:
|
|
303
|
+
file_loc = self.localmod.__file__
|
|
304
|
+
# execfile - this puts any imports in the init file into the globals namespace
|
|
305
|
+
# Note: Anything loose in the module like print statements will be run twice
|
|
306
|
+
exec(compile(open(file_loc).read(), file_loc, "exec"), globs)
|
|
307
|
+
# Call the init command
|
|
308
|
+
init_func = getattr(self.localmod, "init")
|
|
309
|
+
init_func(machine_name)
|
|
310
|
+
|
|
311
|
+
def set_pv_value(
|
|
312
|
+
self,
|
|
313
|
+
name: str,
|
|
314
|
+
value: "PVValue",
|
|
315
|
+
wait: bool = False,
|
|
316
|
+
attempts: int = 3,
|
|
317
|
+
is_local: bool = False,
|
|
318
|
+
) -> None:
|
|
319
|
+
"""
|
|
320
|
+
Set the PV to a value.
|
|
321
|
+
|
|
322
|
+
When setting a PV value this call should be used unless there is a special requirement.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
name: the PV name
|
|
326
|
+
value: the value to set
|
|
327
|
+
wait: wait for the value to be set before returning
|
|
328
|
+
is_local (bool, optional): whether to automatically prepend the
|
|
329
|
+
local inst prefix to the PV name
|
|
330
|
+
attempts: number of attempts to try to set the pv value
|
|
331
|
+
"""
|
|
332
|
+
if is_local:
|
|
333
|
+
if not name.startswith(self.inst_prefix):
|
|
334
|
+
name = self.prefix_pv_name(name)
|
|
335
|
+
self.logger.log_info_msg("set_pv_value %s %s" % (name, str(value)))
|
|
336
|
+
|
|
337
|
+
while True:
|
|
338
|
+
try:
|
|
339
|
+
Wrapper.set_pv_value(name, value, wait=wait)
|
|
340
|
+
return
|
|
341
|
+
except Exception as e:
|
|
342
|
+
attempts -= 1
|
|
343
|
+
if attempts < 1:
|
|
344
|
+
self.logger.log_error_msg("set_pv_value exception {!r}".format(e))
|
|
345
|
+
raise e
|
|
346
|
+
|
|
347
|
+
def get_pv_value(
|
|
348
|
+
self,
|
|
349
|
+
name: str,
|
|
350
|
+
to_string: bool = False,
|
|
351
|
+
attempts: int = 3,
|
|
352
|
+
is_local: bool = False,
|
|
353
|
+
use_numpy: bool | None = None,
|
|
354
|
+
) -> "PVValue":
|
|
355
|
+
"""
|
|
356
|
+
Get the current value of the PV.
|
|
357
|
+
|
|
358
|
+
When getting a PV value this call should be used unless there is a special requirement.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
name: the PV name
|
|
362
|
+
to_string (bool, optional): whether to cast it to a string
|
|
363
|
+
attempts (int, optional): the number of times it tries to read the pv before
|
|
364
|
+
throwing an exception if it
|
|
365
|
+
can not
|
|
366
|
+
is_local (bool, optional): whether to automatically prepend the local inst prefix
|
|
367
|
+
to the PV name
|
|
368
|
+
use_numpy (None|boolean): True use numpy to return arrays, False return a list;
|
|
369
|
+
None for use the default
|
|
370
|
+
"""
|
|
371
|
+
if is_local:
|
|
372
|
+
if not name.startswith(self.inst_prefix):
|
|
373
|
+
name = self.prefix_pv_name(name)
|
|
374
|
+
|
|
375
|
+
if not self.pv_exists(name):
|
|
376
|
+
raise UnableToConnectToPVException(name, "does not exist")
|
|
377
|
+
|
|
378
|
+
while True:
|
|
379
|
+
try:
|
|
380
|
+
return Wrapper.get_pv_value(name, to_string, use_numpy=use_numpy)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
attempts -= 1
|
|
383
|
+
if attempts < 1:
|
|
384
|
+
raise e
|
|
385
|
+
|
|
386
|
+
def pv_exists(self, name: str, fail_fast: bool = False, is_local: bool = False) -> bool:
|
|
387
|
+
"""
|
|
388
|
+
See if the PV exists.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
name (string): the name of the block
|
|
392
|
+
fail_fast (bool, optional): if True the function will not attempt to wait for
|
|
393
|
+
a disconnected PV
|
|
394
|
+
is_local (bool, optional): whether to automatically prepend the local inst prefix
|
|
395
|
+
to the PV name
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
bool: True if the block exists
|
|
399
|
+
"""
|
|
400
|
+
if is_local:
|
|
401
|
+
if not name.startswith(self.inst_prefix):
|
|
402
|
+
name = self.prefix_pv_name(name)
|
|
403
|
+
if fail_fast:
|
|
404
|
+
return Wrapper.pv_exists(name, 0)
|
|
405
|
+
else:
|
|
406
|
+
return Wrapper.pv_exists(name)
|
|
407
|
+
|
|
408
|
+
def connected_pvs_in_list(self, pv_list: list[str], is_local: bool = False) -> list[str]:
|
|
409
|
+
"""
|
|
410
|
+
Checks whether the specified PVs are connected.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
pv_list (list): the list of PVs to check
|
|
414
|
+
is_local (bool, optional): whether to automatically prepend the
|
|
415
|
+
local inst prefix to the PV names
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
bool: True if all PVs are connected
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
# do this with multiprocessing to speed up
|
|
422
|
+
import multiprocessing.dummy as multiprocessing
|
|
423
|
+
|
|
424
|
+
pool = multiprocessing.Pool()
|
|
425
|
+
|
|
426
|
+
# make the mapping work with correct params for
|
|
427
|
+
# def pv_exists(self, name, fail_fast=False, is_local=False):
|
|
428
|
+
# to avoid this eror: def pv_exists(self, name, fail_fast=False, is_local=False):
|
|
429
|
+
def pv_exists_wrapper(name: str) -> str | None:
|
|
430
|
+
if self.pv_exists(name, fail_fast=False, is_local=is_local):
|
|
431
|
+
return name
|
|
432
|
+
else:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
connected_pv_list = [pv for pv in pool.map(pv_exists_wrapper, pv_list) if pv is not None]
|
|
436
|
+
pool.close()
|
|
437
|
+
pool.join()
|
|
438
|
+
return connected_pv_list
|
|
439
|
+
|
|
440
|
+
def reload_current_config(self) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Reload the current configuration.
|
|
443
|
+
"""
|
|
444
|
+
self.blockserver.reload_current_config()
|
|
445
|
+
|
|
446
|
+
def correct_blockname(self, name: str, add_prefix: bool = True) -> str:
|
|
447
|
+
"""
|
|
448
|
+
Corrects the casing of the block.
|
|
449
|
+
"""
|
|
450
|
+
for true_block_name in self.get_block_names():
|
|
451
|
+
if name.lower() == true_block_name.lower():
|
|
452
|
+
if add_prefix:
|
|
453
|
+
return self.inst_prefix + self.block_prefix + true_block_name
|
|
454
|
+
else:
|
|
455
|
+
return true_block_name
|
|
456
|
+
# If we get here then the block does not exist
|
|
457
|
+
# but this should be picked up elsewhere
|
|
458
|
+
return name
|
|
459
|
+
|
|
460
|
+
def get_block_names(self) -> list[str]:
|
|
461
|
+
"""
|
|
462
|
+
Gets a list of block names from the block name monitor.
|
|
463
|
+
|
|
464
|
+
Note: does not include the prefix
|
|
465
|
+
"""
|
|
466
|
+
return [name for name in BLOCK_NAMES.__dict__.keys()]
|
|
467
|
+
|
|
468
|
+
def block_exists(self, name: str, fail_fast: bool = False) -> str | None:
|
|
469
|
+
"""
|
|
470
|
+
Checks whether the block exists.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
name (string): the name of the block
|
|
474
|
+
fail_fast (bool): if True the function will not attempt to wait for a disconnected PV
|
|
475
|
+
|
|
476
|
+
Note: this is case insensitive
|
|
477
|
+
"""
|
|
478
|
+
return self.pv_exists(self.get_pv_from_block(name), fail_fast)
|
|
479
|
+
|
|
480
|
+
def set_block_value(
|
|
481
|
+
self,
|
|
482
|
+
name: str,
|
|
483
|
+
value: "PVValue" = None,
|
|
484
|
+
runcontrol: bool | None = None,
|
|
485
|
+
lowlimit: int | float | None = None,
|
|
486
|
+
highlimit: int | float | None = None,
|
|
487
|
+
wait: bool | None = False,
|
|
488
|
+
) -> None:
|
|
489
|
+
"""
|
|
490
|
+
Sets a range of block values.
|
|
491
|
+
"""
|
|
492
|
+
# Run pre-command
|
|
493
|
+
if wait is not None and runcontrol is not None:
|
|
494
|
+
# Cannot set both at the same time
|
|
495
|
+
raise Exception(
|
|
496
|
+
"Cannot enable or disable runcontrol at the same time as setting a wait"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if not self.pre_post_cmd_manager.cset_precmd(runcontrol=runcontrol, wait=wait):
|
|
500
|
+
print("cset cancelled by pre-command")
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
full_name = self.get_pv_from_block(name)
|
|
504
|
+
|
|
505
|
+
if lowlimit is not None and highlimit is not None:
|
|
506
|
+
if lowlimit > highlimit:
|
|
507
|
+
print(
|
|
508
|
+
"Low limit ({}) higher than high limit ({}), "
|
|
509
|
+
"swapping them around for you".format(lowlimit, highlimit)
|
|
510
|
+
)
|
|
511
|
+
lowlimit, highlimit = highlimit, lowlimit
|
|
512
|
+
if wait and not lowlimit < value < highlimit:
|
|
513
|
+
# Can only warn as may move through this range whilst changing
|
|
514
|
+
print(
|
|
515
|
+
"Warning the range {} to {} does not cover setpoint of {}, "
|
|
516
|
+
"may wait forever".format(lowlimit, highlimit, value)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if value is not None:
|
|
520
|
+
# Write to SP if it exists
|
|
521
|
+
if self.pv_exists(full_name + ":SP"):
|
|
522
|
+
self.set_pv_value(full_name + ":SP", value)
|
|
523
|
+
else:
|
|
524
|
+
self.set_pv_value(full_name, value)
|
|
525
|
+
|
|
526
|
+
if wait:
|
|
527
|
+
self.waitfor.start_waiting(name, value, lowlimit, highlimit)
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
if runcontrol is not None:
|
|
531
|
+
enable = 1 if runcontrol else 0
|
|
532
|
+
self.set_pv_value(full_name + RC_ENABLE, enable)
|
|
533
|
+
|
|
534
|
+
# Set limits
|
|
535
|
+
if lowlimit is not None:
|
|
536
|
+
self.set_pv_value(full_name + RC_LOW, lowlimit)
|
|
537
|
+
if highlimit is not None:
|
|
538
|
+
self.set_pv_value(full_name + RC_HIGH, highlimit)
|
|
539
|
+
|
|
540
|
+
def get_block_value(self, name: str, to_string: bool = False, attempts: int = 3) -> "PVValue":
|
|
541
|
+
"""
|
|
542
|
+
Gets the current value for the block.
|
|
543
|
+
"""
|
|
544
|
+
return self.get_pv_value(self.get_pv_from_block(name), to_string, attempts)
|
|
545
|
+
|
|
546
|
+
def set_multiple_blocks(self, names: list[str], values: list["PVValue"]) -> None:
|
|
547
|
+
"""
|
|
548
|
+
Sets values for multiple blocks.
|
|
549
|
+
"""
|
|
550
|
+
# With LabVIEW we could set values then press go after all values are set
|
|
551
|
+
# Not sure we are going to do something similar for EPICS
|
|
552
|
+
temp = list(zip(names, values))
|
|
553
|
+
# Set the values
|
|
554
|
+
for name, value in temp:
|
|
555
|
+
self.set_block_value(name, value)
|
|
556
|
+
|
|
557
|
+
def get_block_units(self, block_name: str) -> str:
|
|
558
|
+
"""
|
|
559
|
+
Get the physical measurement units associated with a block name.
|
|
560
|
+
|
|
561
|
+
Parameters
|
|
562
|
+
----------
|
|
563
|
+
block_name: name of the block
|
|
564
|
+
|
|
565
|
+
Returns
|
|
566
|
+
-------
|
|
567
|
+
units of the block
|
|
568
|
+
"""
|
|
569
|
+
pv_name = self.get_pv_from_block(block_name)
|
|
570
|
+
if "." in pv_name:
|
|
571
|
+
# Remove any headers
|
|
572
|
+
pv_name = pv_name.split(".")[0]
|
|
573
|
+
unit_name = pv_name + ".EGU"
|
|
574
|
+
# pylint: disable=protected-access
|
|
575
|
+
if not self.block_exists(block_name) and block_name.upper() not in (
|
|
576
|
+
existing_block.upper() for existing_block in self.get_block_names()
|
|
577
|
+
):
|
|
578
|
+
# If block doesn't exist, not found even in some form on the block server
|
|
579
|
+
raise Exception(
|
|
580
|
+
"No block with the name '{}' exists\nCurrent blocks are {}".format(
|
|
581
|
+
block_name, self.get_block_names()
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return Wrapper.get_pv_value(unit_name)
|
|
586
|
+
|
|
587
|
+
def _get_pars(
|
|
588
|
+
self, pv_prefix_identifier: str, get_names_from_blockserver: Callable[[], list[str]]
|
|
589
|
+
) -> dict:
|
|
590
|
+
"""
|
|
591
|
+
Get the current parameter values for a given pv subset as a dictionary.
|
|
592
|
+
"""
|
|
593
|
+
names = get_names_from_blockserver()
|
|
594
|
+
ans = {}
|
|
595
|
+
if names is not None:
|
|
596
|
+
for n in names:
|
|
597
|
+
val = self.get_pv_value(self.prefix_pv_name(n))
|
|
598
|
+
m = re.match(".+:" + pv_prefix_identifier + ":(.+)", n)
|
|
599
|
+
if m is not None:
|
|
600
|
+
ans[m.groups()[0]] = val
|
|
601
|
+
else:
|
|
602
|
+
self.logger.log_error_msg(
|
|
603
|
+
"Unexpected PV found whilst retrieving parameters: {0}".format(n)
|
|
604
|
+
)
|
|
605
|
+
return ans
|
|
606
|
+
|
|
607
|
+
def get_sample_pars(self) -> dict:
|
|
608
|
+
"""
|
|
609
|
+
Get the current sample parameter values as a dictionary.
|
|
610
|
+
"""
|
|
611
|
+
return self._get_pars("SAMPLE", self.blockserver.get_sample_par_names)
|
|
612
|
+
|
|
613
|
+
def set_sample_par(self, name: str, value: "PVValue") -> None:
|
|
614
|
+
"""
|
|
615
|
+
Set a new value for a sample parameter.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
name: the name of the parameter to change
|
|
619
|
+
value: the new value
|
|
620
|
+
"""
|
|
621
|
+
names = self.blockserver.get_sample_par_names()
|
|
622
|
+
if names is not None:
|
|
623
|
+
for n in names:
|
|
624
|
+
m = re.match(".+:SAMPLE:%s" % name.upper(), n)
|
|
625
|
+
if m is not None:
|
|
626
|
+
# Found it!
|
|
627
|
+
self.set_pv_value(self.prefix_pv_name(n), value)
|
|
628
|
+
return
|
|
629
|
+
raise Exception("Sample parameter %s does not exist" % name)
|
|
630
|
+
|
|
631
|
+
def get_beamline_pars(self) -> dict:
|
|
632
|
+
"""
|
|
633
|
+
Get the current beamline parameter values as a dictionary.
|
|
634
|
+
"""
|
|
635
|
+
return self._get_pars("BL", self.blockserver.get_beamline_par_names)
|
|
636
|
+
|
|
637
|
+
def set_beamline_par(self, name: str, value: "PVValue") -> None:
|
|
638
|
+
"""
|
|
639
|
+
Set a new value for a beamline parameter.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
name: the name of the parameter to change
|
|
643
|
+
value: the new value
|
|
644
|
+
"""
|
|
645
|
+
names = self.blockserver.get_beamline_par_names()
|
|
646
|
+
if names is not None:
|
|
647
|
+
for n in names:
|
|
648
|
+
m = re.match(".+:BL:%s" % name.upper(), n)
|
|
649
|
+
if m is not None:
|
|
650
|
+
self.set_pv_value(self.prefix_pv_name(n), value)
|
|
651
|
+
return
|
|
652
|
+
raise Exception("Beamline parameter %s does not exist" % name)
|
|
653
|
+
|
|
654
|
+
def get_runcontrol_settings(self, block_name: str) -> tuple[bool, float, float]:
|
|
655
|
+
"""
|
|
656
|
+
Gets the current run-control settings for a block.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
block_name: the full pv of the block
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
tuple: (enabled, low_limit, high_limit)
|
|
663
|
+
"""
|
|
664
|
+
try:
|
|
665
|
+
block_pv = self.get_pv_from_block(block_name)
|
|
666
|
+
enabled = self.get_pv_value(block_pv + RC_ENABLE) == "YES"
|
|
667
|
+
low_limit = self.get_pv_value(block_pv + RC_LOW)
|
|
668
|
+
high_limit = self.get_pv_value(block_pv + RC_HIGH)
|
|
669
|
+
return enabled, low_limit, high_limit
|
|
670
|
+
except UnableToConnectToPVException:
|
|
671
|
+
return "UNKNOWN", "UNKNOWN", "UNKNOWN"
|
|
672
|
+
|
|
673
|
+
def check_alarms(self, blocks: list[str]) -> tuple[list[str], list[str], list[str]]:
|
|
674
|
+
"""
|
|
675
|
+
Checks whether the specified blocks are in alarm.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
blocks (list): the blocks to check
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
list, list, list: the blocks in minor, major and invalid alarm
|
|
682
|
+
"""
|
|
683
|
+
alarm_states = self._get_fields_from_blocks(blocks, "SEVR", "alarm state")
|
|
684
|
+
minor = [t[0] for t in alarm_states if t[1] == "MINOR"]
|
|
685
|
+
major = [t[0] for t in alarm_states if t[1] == "MAJOR"]
|
|
686
|
+
invalid = [t[0] for t in alarm_states if t[1] == "INVALID"]
|
|
687
|
+
return minor, major, invalid
|
|
688
|
+
|
|
689
|
+
def check_limit_violations(self, blocks: list[str]) -> list[str]:
|
|
690
|
+
"""
|
|
691
|
+
Checks whether the specified blocks have soft limit violations.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
blocks (list): the blocks to check
|
|
695
|
+
|
|
696
|
+
Returns:
|
|
697
|
+
list: the blocks which have soft limit violations
|
|
698
|
+
"""
|
|
699
|
+
violation_states = self._get_fields_from_blocks(blocks, "LVIO", "limit violation")
|
|
700
|
+
return [t[0] for t in violation_states if t[1] == 1]
|
|
701
|
+
|
|
702
|
+
def _get_fields_from_blocks(
|
|
703
|
+
self, blocks: list[str], field_name: str, field_description: str
|
|
704
|
+
) -> list["PVValue"]:
|
|
705
|
+
field_values = list()
|
|
706
|
+
for block in blocks:
|
|
707
|
+
if self.block_exists(block):
|
|
708
|
+
block_name = self.correct_blockname(block, False)
|
|
709
|
+
full_block_pv = self.get_pv_from_block(block)
|
|
710
|
+
try:
|
|
711
|
+
field_value = self.get_pv_value(full_block_pv + "." + field_name, attempts=1)
|
|
712
|
+
field_values.append([block_name, field_value])
|
|
713
|
+
except IOError:
|
|
714
|
+
# Could not get value
|
|
715
|
+
print("Could not get {} for block: {}".format(field_description, block))
|
|
716
|
+
else:
|
|
717
|
+
print("Block {} does not exist, so ignoring it".format(block))
|
|
718
|
+
|
|
719
|
+
return field_values
|
|
720
|
+
|
|
721
|
+
def get_pv_from_block(self, block_name: str) -> str:
|
|
722
|
+
"""
|
|
723
|
+
Get the full gateway level PV name for a given block.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
block_name (str): The name of a block
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
pv_name (str): The pv name as a string
|
|
730
|
+
|
|
731
|
+
"""
|
|
732
|
+
return self.inst_prefix + self.block_prefix + block_name.upper()
|
|
733
|
+
|
|
734
|
+
def _alert_http_request(
|
|
735
|
+
self,
|
|
736
|
+
message: str,
|
|
737
|
+
emails: str | None = None,
|
|
738
|
+
mobiles: str | None = None,
|
|
739
|
+
inst: str | None = None,
|
|
740
|
+
) -> None:
|
|
741
|
+
if emails is None and mobiles is None and inst is None:
|
|
742
|
+
self.logger.log_info_msg(
|
|
743
|
+
"_alert_http_request called with no destinations, doing nothing."
|
|
744
|
+
)
|
|
745
|
+
return
|
|
746
|
+
|
|
747
|
+
pw = self.get_pv_value("CS:AC:ALERTS:PW:SP", to_string=True, is_local=True)
|
|
748
|
+
if not pw:
|
|
749
|
+
raise ValueError(
|
|
750
|
+
"Unable to send sms as cannot get ALERTS password. "
|
|
751
|
+
"Contact ISIS experiment controls for assistance."
|
|
752
|
+
)
|
|
753
|
+
assert isinstance(pw, str)
|
|
754
|
+
req = {
|
|
755
|
+
"message": message,
|
|
756
|
+
"source": "GENIE",
|
|
757
|
+
"type": "ALERT",
|
|
758
|
+
"pw": pw,
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if emails is not None:
|
|
762
|
+
req["emails"] = emails
|
|
763
|
+
if mobiles is not None:
|
|
764
|
+
req["mobiles"] = mobiles
|
|
765
|
+
if inst is not None:
|
|
766
|
+
req["inst"] = inst
|
|
767
|
+
|
|
768
|
+
address = self.get_pv_value("CS:AC:ALERTS:URL:SP", to_string=True, is_local=True)
|
|
769
|
+
if not address:
|
|
770
|
+
raise ValueError(
|
|
771
|
+
"Unable to send sms as cannot get ALERTS http url. "
|
|
772
|
+
"Contact ISIS experiment controls for assistance."
|
|
773
|
+
)
|
|
774
|
+
assert isinstance(address, str)
|
|
775
|
+
|
|
776
|
+
req = urllib.request.Request(url=address, data=urllib.parse.urlencode(req).encode("utf-8"))
|
|
777
|
+
with contextlib.closing(urllib.request.urlopen(req)) as f:
|
|
778
|
+
print(f.read())
|
|
779
|
+
|
|
780
|
+
def send_sms(self, phone_num: str, message: str) -> None:
|
|
781
|
+
"""
|
|
782
|
+
Sends an SMS message to a phone number.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
phone_num (string): The phone number to send the SMS to.
|
|
786
|
+
message (string): The message to send.
|
|
787
|
+
"""
|
|
788
|
+
try:
|
|
789
|
+
self._alert_http_request(mobiles=phone_num, message=message)
|
|
790
|
+
except Exception as e:
|
|
791
|
+
raise Exception("Could not send SMS: {}".format(e))
|
|
792
|
+
|
|
793
|
+
def send_email(self, address: str, message: str) -> None:
|
|
794
|
+
"""
|
|
795
|
+
Sends an email to a given address.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
address (string): The email address to use.
|
|
799
|
+
message (string): The message to send.
|
|
800
|
+
"""
|
|
801
|
+
try:
|
|
802
|
+
self._alert_http_request(emails=address, message=message)
|
|
803
|
+
except Exception as e:
|
|
804
|
+
raise Exception("Could not send email: {}".format(e))
|
|
805
|
+
|
|
806
|
+
def send_alert(self, message: str, inst: str) -> None:
|
|
807
|
+
"""
|
|
808
|
+
Sends an alert message for a specified instrument.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
message (string): The message to send.
|
|
812
|
+
inst (string): The instrument to generate an alert for.
|
|
813
|
+
"""
|
|
814
|
+
if inst is None:
|
|
815
|
+
inst = self.instrument_name
|
|
816
|
+
try:
|
|
817
|
+
self._alert_http_request(inst=inst, message=message)
|
|
818
|
+
except Exception as e:
|
|
819
|
+
raise Exception("Could not send alert: {}".format(e))
|
|
820
|
+
|
|
821
|
+
def get_alarm_from_block(self, block: str) -> str:
|
|
822
|
+
"""
|
|
823
|
+
Gets the alarm status from a single block
|
|
824
|
+
|
|
825
|
+
args:
|
|
826
|
+
block (str): the name of the block to get the alarm status of
|
|
827
|
+
|
|
828
|
+
returns:
|
|
829
|
+
(str) the alarm status as a string.
|
|
830
|
+
One of "NO_ALARM", "MINOR", "MAJOR", "INVALID", or "UNKNOWN" if the
|
|
831
|
+
alarm status could not be determined
|
|
832
|
+
"""
|
|
833
|
+
|
|
834
|
+
return self.get_pv_alarm(self.get_pv_from_block(block))
|
|
835
|
+
|
|
836
|
+
def get_pv_alarm(self, pv_name: str) -> str:
|
|
837
|
+
"""
|
|
838
|
+
Gets the alarm status of a pv.
|
|
839
|
+
|
|
840
|
+
args:
|
|
841
|
+
pv_name (str): the name of the pv to get the alarm status of
|
|
842
|
+
|
|
843
|
+
returns:
|
|
844
|
+
(str) the alarm status as a string.
|
|
845
|
+
One of "NO_ALARM", "MINOR", "MAJOR", "INVALID", or "UNKNOWN" if the
|
|
846
|
+
alarm status could not be determined
|
|
847
|
+
"""
|
|
848
|
+
try:
|
|
849
|
+
return self.get_pv_value(
|
|
850
|
+
"{}.SEVR".format(remove_field_from_pv(pv_name)), to_string=True
|
|
851
|
+
)
|
|
852
|
+
except Exception:
|
|
853
|
+
return "UNKNOWN"
|
|
854
|
+
|
|
855
|
+
def get_block_data(self, block: str, fail_fast: bool = False) -> dict:
|
|
856
|
+
"""
|
|
857
|
+
Gets the useful values associated with a block.
|
|
858
|
+
|
|
859
|
+
The value will be None if the block is not "connected".
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
block (string): the name of the block
|
|
863
|
+
fail_fast (bool): if True the function will not attempt to wait for a disconnected PV
|
|
864
|
+
|
|
865
|
+
Returns
|
|
866
|
+
dict: details about about the block. Contains:
|
|
867
|
+
name - name of the block
|
|
868
|
+
value - value of the block
|
|
869
|
+
unit - physical units of the block
|
|
870
|
+
connected - True if connected; False otherwise
|
|
871
|
+
runcontrol - NO not in runcontrol, YES otherwise
|
|
872
|
+
lowlimit - run control low limit set
|
|
873
|
+
highlimit - run control high limit set
|
|
874
|
+
alarm - the alarm status of the block
|
|
875
|
+
"""
|
|
876
|
+
ans = OrderedDict()
|
|
877
|
+
ans["connected"] = True
|
|
878
|
+
|
|
879
|
+
if not self.block_exists(block, fail_fast):
|
|
880
|
+
# Check if block exists in some form in the block server
|
|
881
|
+
if block.upper() in (
|
|
882
|
+
existing_block.upper() for existing_block in self.get_block_names()
|
|
883
|
+
):
|
|
884
|
+
ans["connected"] = False
|
|
885
|
+
else:
|
|
886
|
+
# Can't find block at all
|
|
887
|
+
raise Exception(
|
|
888
|
+
"No block with the name '{}' exists\nCurrent blocks are {}".format(
|
|
889
|
+
block, self.get_block_names()
|
|
890
|
+
)
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
ans["name"] = block
|
|
894
|
+
ans["value"] = self.get_block_value(block) if ans["connected"] else None
|
|
895
|
+
|
|
896
|
+
try:
|
|
897
|
+
ans["unit"] = self.get_block_units(block) if ans["connected"] else None
|
|
898
|
+
except UnableToConnectToPVException:
|
|
899
|
+
ans["unit"] = "Unable to connect to .EGU PV"
|
|
900
|
+
|
|
901
|
+
ans["runcontrol"], ans["lowlimit"], ans["highlimit"] = self.get_runcontrol_settings(block)
|
|
902
|
+
|
|
903
|
+
fail_fast_and_disconnected = fail_fast and not ans["connected"]
|
|
904
|
+
ans["alarm"] = "UNKNOWN" if fail_fast_and_disconnected else self.get_alarm_from_block(block)
|
|
905
|
+
|
|
906
|
+
return ans
|