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.
Files changed (43) hide show
  1. genie_python/.pylintrc +539 -0
  2. genie_python/__init__.py +1 -0
  3. genie_python/block_names.py +123 -0
  4. genie_python/channel_access_exceptions.py +45 -0
  5. genie_python/genie.py +2462 -0
  6. genie_python/genie_advanced.py +418 -0
  7. genie_python/genie_alerts.py +195 -0
  8. genie_python/genie_api_setup.py +451 -0
  9. genie_python/genie_blockserver.py +64 -0
  10. genie_python/genie_cachannel_wrapper.py +545 -0
  11. genie_python/genie_change_cache.py +151 -0
  12. genie_python/genie_dae.py +2218 -0
  13. genie_python/genie_epics_api.py +906 -0
  14. genie_python/genie_experimental_data.py +186 -0
  15. genie_python/genie_logging.py +200 -0
  16. genie_python/genie_p4p_wrapper.py +203 -0
  17. genie_python/genie_plot.py +77 -0
  18. genie_python/genie_pre_post_cmd_manager.py +21 -0
  19. genie_python/genie_pv_connection_protocol.py +36 -0
  20. genie_python/genie_script_checker.py +507 -0
  21. genie_python/genie_script_generator.py +212 -0
  22. genie_python/genie_simulate.py +69 -0
  23. genie_python/genie_simulate_impl.py +1265 -0
  24. genie_python/genie_startup.py +29 -0
  25. genie_python/genie_toggle_settings.py +58 -0
  26. genie_python/genie_wait_for_move.py +154 -0
  27. genie_python/genie_waitfor.py +576 -0
  28. genie_python/matplotlib_backend/__init__.py +0 -0
  29. genie_python/matplotlib_backend/ibex_websocket_backend.py +366 -0
  30. genie_python/mysql_abstraction_layer.py +272 -0
  31. genie_python/run_tests.py +56 -0
  32. genie_python/scanning_instrument_pylint_plugin.py +31 -0
  33. genie_python/typings/CaChannel/CaChannel.pyi +893 -0
  34. genie_python/typings/CaChannel/__init__.pyi +9 -0
  35. genie_python/typings/CaChannel/_version.pyi +6 -0
  36. genie_python/typings/CaChannel/ca.pyi +31 -0
  37. genie_python/utilities.py +406 -0
  38. genie_python/version.py +1 -0
  39. genie_python-15.1.0rc1.dist-info/LICENSE +28 -0
  40. genie_python-15.1.0rc1.dist-info/METADATA +95 -0
  41. genie_python-15.1.0rc1.dist-info/RECORD +43 -0
  42. genie_python-15.1.0rc1.dist-info/WHEEL +5 -0
  43. 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