Skip to content

Commit

Permalink
Merge pull request #64 from nstelter-slac/display_threshholds
Browse files Browse the repository at this point in the history
Display thresholds
  • Loading branch information
jbellister-slac authored Dec 19, 2023
2 parents db9952c + af3ebf8 commit 4ec8f07
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 0 deletions.
15 changes: 15 additions & 0 deletions slam/alarm_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def __init__(
self.annunciating = annunciating
self.delay = delay
self.alarm_filter = alarm_filter
self.pv_object = None

def is_leaf(self) -> bool:
"""Return whether or not this alarm is associated with a leaf node in its configured hierarchy"""
Expand Down Expand Up @@ -158,6 +159,20 @@ def is_acknowledged(self) -> bool:
AlarmSeverity.UNDEFINED_ACK,
)

def is_undefined_or_invalid(self) -> bool:
"""
A convenience method for returning whether or not this item is undefiend or invalid.
(Basically returns if the alarm-item is colored some shade of purple)
This function is useful to check before using some PyEpics calls like "cainfo",
which takes long time to return when called on undefined/invalid alarms.
"""
return self.alarm_severity in (
AlarmSeverity.UNDEFINED,
AlarmSeverity.UNDEFINED_ACK,
AlarmSeverity.INVALID,
AlarmSeverity.INVALID_ACK,
)

def is_in_active_alarm_state(self) -> bool:
"""A convenience method for returning whether or not this item is actively in an alarm state"""
return self.alarm_severity in (
Expand Down
66 changes: 66 additions & 0 deletions slam/alarm_table_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
QVBoxLayout,
QWidget,
)
from epics import PV
from typing import Callable, List
from .alarm_table_model import AlarmItemsTableModel
from .alarm_tree_model import AlarmItemsTreeModel
from .permissions import UserAction, can_take_action
from math import isnan


class AlarmTableType(str, enum.Enum):
Expand Down Expand Up @@ -108,6 +110,7 @@ def __init__(
self.unacknowledge_action = QAction("Unacknowledge")
self.copy_action = QAction("Copy PV To Clipboard")
self.plot_action = QAction("Draw Plot")
self.display_thresholds_menu = QMenu("Display Alarm Thresholds")
self.acknowledge_action.triggered.connect(partial(self.send_acknowledge_action, True))
self.unacknowledge_action.triggered.connect(partial(self.send_acknowledge_action, False))
self.plot_action.triggered.connect(self.plot_pv)
Expand All @@ -120,6 +123,8 @@ def __init__(

self.alarm_context_menu.addAction(self.copy_action)
self.alarm_context_menu.addAction(self.plot_action)
self.alarm_context_menu.addMenu(self.display_thresholds_menu)
self.display_thresholds_menu.aboutToShow.connect(self.handleThresholdDisplay)

self.alarmView.contextMenuEvent = self.alarm_context_menu_event

Expand All @@ -145,6 +150,67 @@ def __init__(

self.alarmModel.layoutChanged.connect(self.update_counter_label)

def handleThresholdDisplay(self):
indices = self.get_selected_indices()
alarm_item = None
hihi = high = low = lolo = -1

# If multiple alarm-items selected, just display thresholds for 1st item.
# (or don't display anything if 1st item is undefined/invalid).
# This follows how the "Draw Plot" option handles multiple selected items.
if len(indices) > 0:
index = indices[0]
alarm_item = list(self.alarmModel.alarm_items.items())[index.row()][1]
else:
return

# If not a leaf its an invalid 'cainfo' call which could stall things for a while.
if not alarm_item.is_leaf():
# Don't display any of the threshold-display actions if selected non-leaf
self.display_thresholds_menu.clear()
return

# Avoid calling 'cainfo' on undefined alarm since causes the call to stall for a bit.
# Also we don't want thresholds from an undefined alarm anyway.
if alarm_item.is_undefined_or_invalid():
# Don't display any of the threshold-display actions if alarm-item undefined
self.display_thresholds_menu.clear()
return

if alarm_item.pv_object is None:
alarm_item.pv_object = PV(alarm_item.name)
# Update the values only when user requests them in right-click menu
alarm_item.pv_object.clear_auto_monitor()

alarm_item_metadata = alarm_item.pv_object.get_ctrlvars()

# Getting data can fail for some PV's, good metadata will always have a key for all 4 limits (nan if not set),
# in this case don't display any threshold sub-menus
if (
alarm_item_metadata is not None
and len(alarm_item_metadata) > 0
and "upper_alarm_limit" not in alarm_item_metadata
):
self.display_thresholds_menu.clear()
return

# threshold values are not always set, just display "None" if so
# upper_alarm_limit here is same as calling caget for pv's '.HIHI'
hihi = alarm_item_metadata["upper_alarm_limit"]
lolo = alarm_item_metadata["lower_alarm_limit"]
high = alarm_item_metadata["upper_warning_limit"]
low = alarm_item_metadata["lower_warning_limit"]

# we display threshold values as 4 items in a drop-down menu
self.hihi_action = QAction("HIHI: " + str(hihi)) if not isnan(hihi) else QAction("HIHI: Not set")
self.high_action = QAction("HIGH: " + str(high)) if not isnan(high) else QAction("HIGH: Not set")
self.low_action = QAction("LOW: " + str(low)) if not isnan(low) else QAction("LOW: Not set")
self.lolo_action = QAction("LOLO: " + str(lolo)) if not isnan(lolo) else QAction("LOLO: Not set")
self.display_thresholds_menu.addAction(self.hihi_action)
self.display_thresholds_menu.addAction(self.high_action)
self.display_thresholds_menu.addAction(self.low_action)
self.display_thresholds_menu.addAction(self.lolo_action)

def filter_table(self) -> None:
"""Filter the table based on the text typed into the filter bar"""
if self.first_filter:
Expand Down
68 changes: 68 additions & 0 deletions slam/alarm_tree_view.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import getpass
import socket
import logging
from functools import partial
from kafka.producer import KafkaProducer
from pydm.display import load_file
Expand All @@ -11,6 +12,10 @@
from .alarm_item import AlarmItem, AlarmSeverity
from .alarm_tree_model import AlarmItemsTreeModel
from .permissions import UserAction, can_take_action
from math import isnan
from epics import PV

logger = logging.getLogger(__name__)


class AlarmTreeViewWidget(QWidget):
Expand Down Expand Up @@ -73,6 +78,7 @@ def __init__(
self.enable_action = QAction("Enable")
self.disable_action = QAction("Disable")
self.guidance_menu = QMenu("Guidance")
self.display_thresholds_menu = QMenu("Display Alarm Thresholds")
self.display_actions = []
self.guidance_objects = []

Expand All @@ -87,6 +93,66 @@ def __init__(

self.layout.addWidget(self.tree_view)

def handleThresholdDisplay(self):
indices = self.tree_view.selectedIndexes()
index = indices[0]
alarm_item = self.treeModel.getItem(index)
hihi = high = low = lolo = -1

# If not a leaf its an invalid 'cainfo' call which could stall things for a while.
if not alarm_item.is_leaf():
# Don't display any of the threshold-display actions if selected non-leaf
self.display_thresholds_menu.clear()
return

# Avoid calling 'cainfo' on undefined alarm since causes the call to stall for a bit.
# Also we don't want thresholds from an undefined alarm anyway.
if alarm_item.is_undefined_or_invalid():
# Don't display any of the threshold-display actions if alarm-item undefined
self.display_thresholds_menu.clear()
return

# Make pv_object if first time item's threshold is requested
if alarm_item.pv_object is None:
# Update the values only when user requests them in right-click menu
alarm_item.pv_object = PV(alarm_item.name, auto_monitor=False)

# Do a get call we can quickly timeout, so if PV not-connected don't
# need to wait for slower get_ctrlvars() call.
# 0.1 is small arbitrary value, can be made larger if timing-out for
# actually connected PVs.
if alarm_item.pv_object.get(timeout=0.1) is None:
return
alarm_item_metadata = alarm_item.pv_object.get_ctrlvars()

# Getting data can fail for some PV's, good metadata will always have a key for all 4 limits (nan if not set),
# in this case don't display any threshold sub-menus
if alarm_item_metadata is None:
logger.warn(f"Can't connect to PV: {alarm_item.name}")
self.display_thresholds_menu.clear()
return
elif len(alarm_item_metadata) > 0 and "upper_alarm_limit" not in alarm_item_metadata:
logger.warn(f"No threshold data for PV: {alarm_item.name}")
self.display_thresholds_menu.clear()
return

# threshold values are not always set, just display "None" if so
# upper_alarm_limit here is same as calling caget for pv's '.HIHI'
hihi = alarm_item_metadata["upper_alarm_limit"]
lolo = alarm_item_metadata["lower_alarm_limit"]
high = alarm_item_metadata["upper_warning_limit"]
low = alarm_item_metadata["lower_warning_limit"]

# we display threshold values as 4 items in a drop-down menu
self.hihi_action = QAction("HIHI: " + str(hihi)) if not isnan(hihi) else QAction("HIHI: Not set")
self.high_action = QAction("HIGH: " + str(high)) if not isnan(high) else QAction("HIGH: Not set")
self.low_action = QAction("LOW: " + str(low)) if not isnan(low) else QAction("LOW: Not set")
self.lolo_action = QAction("LOLO: " + str(lolo)) if not isnan(lolo) else QAction("LOLO: Not set")
self.display_thresholds_menu.addAction(self.hihi_action)
self.display_thresholds_menu.addAction(self.high_action)
self.display_thresholds_menu.addAction(self.low_action)
self.display_thresholds_menu.addAction(self.lolo_action)

def tree_menu(self, pos: QPoint) -> None:
"""Creates and displays the context menu to be displayed upon right clicking on an alarm item"""
indices = self.tree_view.selectedIndexes()
Expand Down Expand Up @@ -140,6 +206,8 @@ def tree_menu(self, pos: QPoint) -> None:
self.context_menu.addAction(self.enable_action)
self.context_menu.addAction(self.disable_action)
self.context_menu.addMenu(self.guidance_menu)
self.context_menu.addMenu(self.display_thresholds_menu)
self.display_thresholds_menu.aboutToShow.connect(self.handleThresholdDisplay)

# Make the entires from the config-page appear when alarm in tree is right-clicked
indices = self.tree_view.selectedIndexes()
Expand Down

0 comments on commit 4ec8f07

Please sign in to comment.