diff --git a/slam/alarm_configuration_widget.py b/slam/alarm_configuration_widget.py index 022b759..52e4ae0 100644 --- a/slam/alarm_configuration_widget.py +++ b/slam/alarm_configuration_widget.py @@ -40,7 +40,12 @@ class AlarmConfigurationWidget(QDialog): """ def __init__( - self, alarm_item: AlarmItem, kafka_producer: KafkaProducer, topic: str, parent: Optional[QObject] = None + self, + alarm_item: AlarmItem, + kafka_producer: KafkaProducer, + topic: str, + annunciate: bool = False, + parent: Optional[QObject] = None, ): super().__init__(parent=parent) self.alarm_item = alarm_item @@ -60,10 +65,10 @@ def __init__( self.behavior_label = QLabel("Behavior:") self.enabled_checkbox = QCheckBox("Enabled") - self.enabled_checkbox.clicked.connect(self.update_enabled_checkbox_pre_disabled_value) - self.latch_checkbox = QCheckBox("Latched") - self.annunciate_checkbox = QCheckBox("Annunciate") + annunciate_checkbox_text = "Annunciate" if annunciate else "Annunciate (disabled)" + self.annunciate_checkbox = QCheckBox(annunciate_checkbox_text) + self.annunciate_checkbox.setEnabled(annunciate) self.disable_date_label = QLabel("Disable Until:") self.minimum_datetime = QDateTime.currentDateTime().addDays(-1) diff --git a/slam/alarm_table_view.py b/slam/alarm_table_view.py index b8fba16..fa78ee3 100644 --- a/slam/alarm_table_view.py +++ b/slam/alarm_table_view.py @@ -61,6 +61,7 @@ def __init__( topic: str, table_type: AlarmTableType, plot_slot: Callable, + annunciate: bool = False, ): super().__init__() self.resize(1035, 600) diff --git a/slam/alarm_tree_model.py b/slam/alarm_tree_model.py index 8c5951b..5b2dec5 100644 --- a/slam/alarm_tree_model.py +++ b/slam/alarm_tree_model.py @@ -18,7 +18,7 @@ class AlarmItemsTreeModel(QAbstractItemModel): The parent of this model. """ - def __init__(self, enable_all_topic: bool = False, parent: Optional[QObject] = None): + def __init__(self, annunciate: bool = False, enable_all_topic: bool = False, parent: Optional[QObject] = None): super().__init__(parent) self.root_item = AlarmItem("") self.nodes = [] @@ -27,6 +27,7 @@ def __init__(self, enable_all_topic: bool = False, parent: Optional[QObject] = N if self.enable_all_topic: self.nodes.insert(0, self.root_item) self.added_paths = dict() # Mapping from PV name to all associated paths in the tree (will be just 1 for most) + self.annunciate = annunciate def clear(self) -> None: """Clear out all the nodes in this tree and set the root to an empty item""" @@ -165,6 +166,13 @@ def update_item( item_to_update.filtered = True elif item_to_update.filtered: item_to_update.filtered = False + # also ensure annunciate is enabled on application level (self.annunciate) and also for the current item. + if item_to_update.is_in_active_alarm_state() and (self.annunciate and item_to_update.annunciating): + # prints bell character, cross platform way to generate "beep" noise + # (assuming the user has the bell-sound option enabled for their terminal), + # could be replaced with call to audio library for more sound options + print("\a") + self.layoutChanged.emit() def update_model(self, item_path: str, values: dict) -> None: diff --git a/slam/alarm_tree_view.py b/slam/alarm_tree_view.py index e12ee3b..98952f4 100644 --- a/slam/alarm_tree_view.py +++ b/slam/alarm_tree_view.py @@ -37,6 +37,7 @@ def __init__( topic: str, plot_slot: Callable, enable_all_topic: bool = False, + annunciate: bool = False, ): super().__init__() @@ -45,10 +46,12 @@ def __init__( self.plot_slot = plot_slot self.plot_signal.connect(self.plot_slot) self.clipboard = QApplication.clipboard() + self.annunciate = annunciate self.setFont(QFont("Arial", 12)) self.layout = QVBoxLayout(self) - self.treeModel = AlarmItemsTreeModel(enable_all_topic) + + self.treeModel = AlarmItemsTreeModel(annunciate, enable_all_topic) self.tree_view = QTreeView(self) self.tree_view.setProperty("showDropIndicator", False) self.tree_view.setDragDropOverwriteMode(False) @@ -179,7 +182,11 @@ def create_alarm_configuration_widget(self, index: QModelIndex) -> None: """Create and display the alarm configuration widget for the alarm item with the input index""" alarm_item = self.treeModel.getItem(index) alarm_config_window = AlarmConfigurationWidget( - alarm_item=alarm_item, kafka_producer=self.kafka_producer, topic=self.topic, parent=self + alarm_item=alarm_item, + kafka_producer=self.kafka_producer, + topic=self.topic, + parent=self, + annunciate=self.annunciate, ) alarm_config_window.show() diff --git a/slam/main_window.py b/slam/main_window.py index 7095568..22f15e4 100644 --- a/slam/main_window.py +++ b/slam/main_window.py @@ -32,7 +32,7 @@ class AlarmHandlerMainWindow(QMainWindow): alarm_update_signal = Signal(str, str, str, AlarmSeverity, str, datetime, str, AlarmSeverity, str) - def __init__(self, topics: List[str], bootstrap_servers: List[str]): + def __init__(self, topics: List[str], bootstrap_servers: List[str], annunciate: bool = False): super().__init__() self.kafka_producer = None @@ -104,12 +104,23 @@ def __init__(self, topics: List[str], bootstrap_servers: List[str]): for topic in topics: self.last_received_update_time[topic] = datetime.now() self.alarm_select_combo_box.addItem(topic) - self.alarm_trees[topic] = AlarmTreeViewWidget(self.kafka_producer, topic, self.plot_pv, False) + + self.alarm_trees[topic] = AlarmTreeViewWidget(self.kafka_producer, topic, self.plot_pv, False, annunciate) self.active_alarm_tables[topic] = AlarmTableViewWidget( - self.alarm_trees[topic].treeModel, self.kafka_producer, topic, AlarmTableType.ACTIVE, self.plot_pv + self.alarm_trees[topic].treeModel, + self.kafka_producer, + topic, + AlarmTableType.ACTIVE, + self.plot_pv, + annunciate, ) self.acknowledged_alarm_tables[topic] = AlarmTableViewWidget( - self.alarm_trees[topic].treeModel, self.kafka_producer, topic, AlarmTableType.ACKNOWLEDGED, self.plot_pv + self.alarm_trees[topic].treeModel, + self.kafka_producer, + topic, + AlarmTableType.ACKNOWLEDGED, + self.plot_pv, + annunciate, ) # Sync the column widths in the active and acknowledged tables, resizing a column will effect both tables. diff --git a/slam/tests/test_alarm_configuration_widget.py b/slam/tests/test_alarm_configuration_widget.py index 1cfd3e2..31715b0 100644 --- a/slam/tests/test_alarm_configuration_widget.py +++ b/slam/tests/test_alarm_configuration_widget.py @@ -27,14 +27,17 @@ def test_create_and_show(qtbot, alarm_item, mock_kafka_producer): def test_save_configuration(qtbot, alarm_item, mock_kafka_producer, enabled, latching, annunciating): """Verify that the information saved in the configuration widget is sent to the kafka cluster correctly""" alarm_config_widget = AlarmConfigurationWidget( - alarm_item=alarm_item, kafka_producer=mock_kafka_producer, topic="TEST" + alarm_item=alarm_item, kafka_producer=mock_kafka_producer, topic="TEST", annunciate=annunciating ) qtbot.addWidget(alarm_config_widget) # Simulate the user typing in several suggestions for how to handle this particular alarm alarm_config_widget.enabled_checkbox.setChecked(enabled) alarm_config_widget.latch_checkbox.setChecked(latching) - alarm_config_widget.annunciate_checkbox.setChecked(annunciating) + if annunciating: + alarm_config_widget.annunciate_checkbox.setChecked(True) + else: + assert alarm_config_widget.annunciate_checkbox.isEnabled() is False alarm_config_widget.guidance_table.cellWidget(0, 0).setText("Call") alarm_config_widget.guidance_table.cellWidget(0, 1).setText("Somebody") diff --git a/slam/tests/test_alarm_tree_model.py b/slam/tests/test_alarm_tree_model.py index 4d45289..18826c8 100644 --- a/slam/tests/test_alarm_tree_model.py +++ b/slam/tests/test_alarm_tree_model.py @@ -1,5 +1,7 @@ from ..alarm_item import AlarmItem, AlarmSeverity from operator import attrgetter +import sys +from io import StringIO def test_clear(tree_model, alarm_item): @@ -145,3 +147,45 @@ def test_remove_item(tree_model): assert len(tree_model.nodes) == 0 assert len(tree_model.added_paths) == 0 + + +def test_annunciation(tree_model): + """Test that a beep noise is made when an alarm-item enters an active alarm state""" + + tree_model.annunciate = True + alarm_item = AlarmItem( + "TEST:PV", + path="/path/to/TEST:PV", + alarm_severity=AlarmSeverity.OK, + alarm_status="OK", + pv_severity=AlarmSeverity.OK, + annunciating=True, + ) + tree_model.nodes.append(alarm_item) + tree_model.added_paths["TEST:PV"] = ["/path/to/TEST:PV"] + + # To verify the beep happened we check that bell character was printed to stdout, + # which when printed to stdout makes beep sound + # (assuming the user has the bell-sound option enabled for their terminal). + # Could later replace with audio library if more sound options are wanted. + stdout_buffer = StringIO() + # redirect stdout to buffer + sys.stdout = stdout_buffer + + tree_model.update_item( + "TEST:PV", + "/path/to/TEST:PV", + AlarmSeverity.MINOR, + "STATE_ALARM", + None, + "FAULT", + AlarmSeverity.MINOR, + "alarm_status", + ) + + # restore original stdout stream + sys.stdout = sys.__stdout__ + + captured_output = stdout_buffer.getvalue() + # checking for bell character + assert captured_output == "\x07\n" diff --git a/slam_launcher/main.py b/slam_launcher/main.py index 1124105..eb6c5c4 100644 --- a/slam_launcher/main.py +++ b/slam_launcher/main.py @@ -8,14 +8,18 @@ def main(): parser = argparse.ArgumentParser(description="SLAC Alarm Manager") - parser.add_argument("--topics", help="Comma separated list of kafka alarm topics to listen to") + parser.add_argument("-t", "--topics", help="Comma separated list of kafka alarm topics to listen to") parser.add_argument( + "-b", "--bootstrap-servers", default="localhost:9092", help="Comma separated list of urls for one or more kafka boostrap servers", ) - parser.add_argument("--user-permissions", default="admin", help="One of read-only, operator, admin") - parser.add_argument("--log", default="warning", help="Logging level. debug, info, warning, error, critical") + parser.add_argument("-u", "--user-permissions", default="admin", help="One of read-only, operator, admin") + parser.add_argument("-l", "--log", default="warning", help="Logging level. debug, info, warning, error, critical") + parser.add_argument( + "-a", "--annunciate", action="store_true", help="Enable beep from alarms that have annunciate setting enabled" + ) # default=False app_args = parser.parse_args() @@ -36,7 +40,7 @@ def main(): topics = app_args.topics.split(",") app = QApplication([]) - main_window = AlarmHandlerMainWindow(topics, kafka_boostrap_servers) + main_window = AlarmHandlerMainWindow(topics, kafka_boostrap_servers, app_args.annunciate) main_window.resize(1536, 864) main_window.setWindowTitle("SLAC Alarm Manager") main_window.show()