Skip to content

Commit ca35c53

Browse files
committed
f
1 parent cdc2a04 commit ca35c53

9 files changed

Lines changed: 114 additions & 24 deletions

File tree

bec_lib/bec_lib/messages.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1744,7 +1744,15 @@ class NotificationConfigMessage(BECMessage):
17441744

17451745
msg_type: ClassVar[str] = "notification_config_message"
17461746
routes: dict[
1747-
Literal["new_scan", "scan_completed", "alarm", "scan_interlock"] | str,
1747+
Literal[
1748+
"new_scan",
1749+
"scan_completed",
1750+
"alarm_warning",
1751+
"alarm_minor",
1752+
"alarm_major",
1753+
"scan_interlock",
1754+
]
1755+
| str,
17481756
list[NotificationServiceTarget],
17491757
] = Field(default_factory=dict)
17501758

@@ -1754,7 +1762,7 @@ class NotificationMessage(BECMessage):
17541762
Message for notifications to be sent to messaging services.
17551763
17561764
Args:
1757-
event (str): Event name, e.g. "new_scan", "scan_completed", "alarm", "scan_interlock"
1765+
event (str): Event name, e.g. "new_scan", "scan_completed", "alarm_warning", "alarm_minor", "alarm_major", "scan_interlock"
17581766
content (dict): Content of the notification message, can contain any relevant information about the event
17591767
metadata (dict, optional): Additional metadata. Defaults to None.
17601768
"""

bec_lib/bec_lib/messaging_hooks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ class MessagingEvent(str, enum.Enum):
2929

3030
SCAN = "new_scan"
3131
SCAN_COMPLETED = "scan_completed"
32-
ALARM = "alarm"
32+
ALARM_WARNING = "alarm_warning"
33+
ALARM_MINOR = "alarm_minor"
34+
ALARM_MAJOR = "alarm_major"
3335
SCAN_INTERLOCK = "scan_interlock"
3436

3537

bec_lib/bec_lib/messaging_services.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,25 @@ def set_default_scope(self, scope: str | list[str] | None) -> None:
199199

200200
def set_auto_notifications(
201201
self,
202-
event_type: Literal["new_scan", "scan_completed", "alarm", "scan_interlock"] | str,
202+
event_type: (
203+
Literal[
204+
"new_scan",
205+
"scan_completed",
206+
"alarm_warning",
207+
"alarm_minor",
208+
"alarm_major",
209+
"scan_interlock",
210+
]
211+
| str
212+
),
203213
enabled: bool,
204214
scopes: list[str] | str | None = None,
205215
) -> None:
206216
"""
207217
Set automatic notifications for a specific event type.
208218
209219
Args:
210-
event_type (Literal["new_scan", "scan_completed", "alarm", "scan_interlock"] | str): The type of event to set notifications for.
220+
event_type (Literal["new_scan", "scan_completed", "alarm_warning", "alarm_minor", "alarm_major", "scan_interlock"] | str): The type of event to set notifications for.
211221
enabled (bool): Whether to enable or disable notifications for the event.
212222
scopes (list[str] | str | None): The scopes to apply the notifications to.
213223
"""

bec_lib/bec_lib/redis_connector.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,12 @@ def raise_alarm(self, severity: Alarms, info: ErrorInfo, metadata: dict | None =
659659
alarm_msg = AlarmMessage(severity=severity, info=info, metadata=metadata or {})
660660
self.set_and_publish(MessageEndpoints.alarm(), alarm_msg)
661661
compact_message = info.compact_error_message or info.error_message or info.exception_type
662-
self.notify(MessagingEvent.ALARM, compact_message)
662+
event_by_severity = {
663+
0: MessagingEvent.ALARM_WARNING,
664+
1: MessagingEvent.ALARM_MINOR,
665+
2: MessagingEvent.ALARM_MAJOR,
666+
}
667+
self.notify(event_by_severity[int(severity)], compact_message)
663668

664669
def notify(
665670
self,

bec_lib/tests/test_messaging_hooks.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,17 @@ def test_messaging_manager_routes_generic_notification_to_signal_as_plain_text(
122122
messaging_manager.on_notification_config(
123123
messages.NotificationConfigMessage(
124124
routes={
125-
"alarm": [messages.NotificationServiceTarget(service_name="signal", scope="ops")]
125+
"alarm_major": [
126+
messages.NotificationServiceTarget(service_name="signal", scope="ops")
127+
]
126128
}
127129
)
128130
)
129131

130132
messaging_manager.on_notification(
131-
MessagingEvent.ALARM,
133+
MessagingEvent.ALARM_MAJOR,
132134
messages.NotificationMessage(
133-
event="alarm",
135+
event="alarm_major",
134136
message=messages.NotificationMessageObject()
135137
.add_text("Beamline checks failed", bold=True, color="red")
136138
.add_tags(["alarm"])
@@ -165,7 +167,9 @@ def test_handle_notification_routes_message_from_topic_suffix(messaging_manager)
165167

166168
def test_handle_notification_config_forwards_message_value(messaging_manager):
167169
config_message = messages.NotificationConfigMessage(
168-
routes={"alarm": [messages.NotificationServiceTarget(service_name="signal", scope="ops")]}
170+
routes={
171+
"alarm_minor": [messages.NotificationServiceTarget(service_name="signal", scope="ops")]
172+
}
169173
)
170174
msg_obj = MessageObject(MessageEndpoints.notification_config().endpoint, config_message)
171175

@@ -196,7 +200,7 @@ def test_to_service_message_for_teams_strips_html_and_ignores_non_text(messaging
196200
{"data": available_services}
197201
)
198202
message = messages.NotificationMessage(
199-
event="alarm",
203+
event="alarm_major",
200204
message=messages.NotificationMessageObject()
201205
.add_text("Beamline checks failed", bold=True, color="red")
202206
.add_tags(["alarm"])
@@ -221,7 +225,7 @@ def test_to_service_message_for_signal_preserves_files_and_plain_text(messaging_
221225
filename="alarm.png", mime_type="image/png", data=b"png-bytes", width=128, height=64
222226
)
223227
message = messages.NotificationMessage(
224-
event="alarm",
228+
event="alarm_warning",
225229
message=messages.NotificationMessageObject()
226230
.add_text("Beamline checks failed", italic=True)
227231
.add_tags(["alarm"])
@@ -241,7 +245,7 @@ def test_to_service_message_for_signal_preserves_files_and_plain_text(messaging_
241245

242246
def test_to_service_message_raises_for_unsupported_service(messaging_manager):
243247
message = messages.NotificationMessage(
244-
event="alarm",
248+
event="alarm_minor",
245249
message=messages.NotificationMessageObject().add_text("Beamline checks failed")._content,
246250
)
247251

bec_lib/tests/test_messaging_service.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ def test_notification_message_object_to_scilog_message(scilog_service):
354354
try:
355355
scilog_message = manager.to_service_message(
356356
scilog_service,
357-
messages.NotificationMessage(event="alarm", message=notification._content),
357+
messages.NotificationMessage(event="alarm_major", message=notification._content),
358358
)
359359
finally:
360360
manager.shutdown()
@@ -386,7 +386,7 @@ def test_notification_message_object_to_signal_message(signal_service):
386386
try:
387387
signal_message = manager.to_service_message(
388388
signal_service,
389-
messages.NotificationMessage(event="alarm", message=notification._content),
389+
messages.NotificationMessage(event="alarm_major", message=notification._content),
390390
)
391391
finally:
392392
manager.shutdown()
@@ -717,7 +717,7 @@ def test_messaging_service_tracks_external_notification_config_updates(
717717
messages.NotificationServiceTarget(service_name="signal", scope="beamline-ops"),
718718
messages.NotificationServiceTarget(service_name="scilog", scope="default"),
719719
],
720-
"alarm": [
720+
"alarm_major": [
721721
messages.NotificationServiceTarget(
722722
service_name="scilog", scope=["default", "secondary"]
723723
)
@@ -730,13 +730,13 @@ def test_messaging_service_tracks_external_notification_config_updates(
730730
while (
731731
time.time() < deadline
732732
and scilog_service._auto_notifications # pylint: disable=protected-access
733-
!= {"new_scan": ["default"], "alarm": ["default", "secondary"]}
733+
!= {"new_scan": ["default"], "alarm_major": ["default", "secondary"]}
734734
):
735735
time.sleep(0.01)
736736

737737
assert scilog_service._auto_notifications == { # pylint: disable=protected-access
738738
"new_scan": ["default"],
739-
"alarm": ["default", "secondary"],
739+
"alarm_major": ["default", "secondary"],
740740
}
741741

742742

bec_lib/tests/test_redis_connector.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,37 @@ def test_redis_connector_send_client_info(connector):
6464

6565

6666
@pytest.mark.parametrize(
67-
"severity, alarm_type, msg, compact_msg, metadata",
67+
"severity, expected_event, alarm_type, msg, compact_msg, metadata",
6868
[
69-
[Alarms.MAJOR, "alarm", "content1", "compact_msg", {"metadata": "metadata1"}],
70-
[Alarms.MINOR, "alarm", "content1", "compact_msg", {"metadata": "metadata1"}],
71-
[Alarms.WARNING, "alarm", "content1", "compact_msg", {"metadata": "metadata1"}],
69+
[
70+
Alarms.MAJOR,
71+
MessagingEvent.ALARM_MAJOR,
72+
"alarm",
73+
"content1",
74+
"compact_msg",
75+
{"metadata": "metadata1"},
76+
],
77+
[
78+
Alarms.MINOR,
79+
MessagingEvent.ALARM_MINOR,
80+
"alarm",
81+
"content1",
82+
"compact_msg",
83+
{"metadata": "metadata1"},
84+
],
85+
[
86+
Alarms.WARNING,
87+
MessagingEvent.ALARM_WARNING,
88+
"alarm",
89+
"content1",
90+
"compact_msg",
91+
{"metadata": "metadata1"},
92+
],
7293
],
7394
)
74-
def test_redis_connector_raise_alarm(connector, severity, alarm_type, msg, compact_msg, metadata):
95+
def test_redis_connector_raise_alarm(
96+
connector, severity, expected_event, alarm_type, msg, compact_msg, metadata
97+
):
7598
with (
7699
mock.patch.object(connector, "set_and_publish", return_value=None),
77100
mock.patch.object(connector, "notify", return_value=None),
@@ -84,7 +107,7 @@ def test_redis_connector_raise_alarm(connector, severity, alarm_type, msg, compa
84107
connector.set_and_publish.assert_called_once_with(
85108
MessageEndpoints.alarm(), AlarmMessage(severity=severity, info=info, metadata=metadata)
86109
)
87-
connector.notify.assert_called_once_with(MessagingEvent.ALARM, compact_msg)
110+
connector.notify.assert_called_once_with(expected_event, compact_msg)
88111

89112

90113
@pytest.mark.parametrize(

bec_server/bec_server/actors/scan_interlock.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
ScanInterlockModifyStateTableMessage,
77
ScanInterlockStateTableContent,
88
)
9+
from bec_lib.messaging_hooks import MessagingEvent
10+
from bec_lib.messaging_services import NotificationMessageObject
911
from bec_server.actors.actor import BlStateActor
1012

1113
logger = bec_logger.logger
@@ -82,6 +84,13 @@ def mismatched_states(self):
8284
]
8385

8486
def some_mismatch_action(self, client: BECClient):
87+
notification = NotificationMessageObject()
88+
notification.add_text(
89+
f"Scan interlock triggered for beamline states: {self.mismatched_states}", color="red"
90+
)
91+
notification.add_tags("scan_interlock")
92+
self.client.connector.notify(MessagingEvent.SCAN_INTERLOCK, notification)
93+
8594
if self.client.queue is None:
8695
return
8796
self.client.queue.add_queue_lock(
@@ -100,6 +109,10 @@ def _unlock(self):
100109
if (q := self.client.queue) is not None:
101110
if (curr_q := q.queue_storage.current_scan_queue) is not None:
102111
if (primary := curr_q.get("primary")) is not None and primary.locks != []:
112+
notification = NotificationMessageObject()
113+
notification.add_text("Scan interlock cleared", color="green")
114+
notification.add_tags("scan_interlock")
115+
self.client.connector.notify(MessagingEvent.SCAN_INTERLOCK, notification)
103116
self.client.queue.remove_queue_lock(queue="primary", lock_id=self._LOCK_ID)
104117

105118
def run(self):

bec_server/tests/tests_scan_server/test_scan_interlock.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import pytest
55

6+
from bec_lib.messaging_hooks import MessagingEvent
7+
from bec_lib.messaging_services import NotificationMessageObject
68
from bec_server.actors.scan_interlock import ScanInterlockActor
79

810

@@ -94,6 +96,11 @@ def test_on_state_modification_remove_missing(self, actor, mock_client):
9496

9597
def test_some_mismatch_action_adds_lock(self, actor, mock_client):
9698
actor.some_mismatch_action(mock_client)
99+
mock_client.connector.notify.assert_called_once()
100+
event, notification = mock_client.connector.notify.call_args.args
101+
assert event == MessagingEvent.SCAN_INTERLOCK
102+
assert isinstance(notification, NotificationMessageObject)
103+
assert notification._content[1].tags == ["scan_interlock"]
97104
mock_client.queue.add_queue_lock.assert_called_once_with(
98105
queue="primary",
99106
reason="Interlock for beamline states: []",
@@ -104,6 +111,7 @@ def test_some_mismatch_action_skips_if_no_queue(self, actor, mock_client):
104111
add_queue_lock = mock_client.queue.add_queue_lock
105112
actor.client.queue = None
106113
actor.some_mismatch_action(mock_client)
114+
mock_client.connector.notify.assert_called_once()
107115
add_queue_lock.assert_not_called()
108116

109117
def test_all_match_action_unlocks(self, actor):
@@ -114,6 +122,23 @@ def test_all_match_action_unlocks(self, actor):
114122

115123
def test_unlock_removes_lock(self, actor, mock_client):
116124
actor._unlock()
125+
mock_client.connector.notify.assert_called_once()
126+
event, notification = mock_client.connector.notify.call_args.args
127+
assert event == MessagingEvent.SCAN_INTERLOCK
128+
assert isinstance(notification, NotificationMessageObject)
129+
assert (
130+
notification._content[0].content
131+
== '<p><mark class="pen-green">Scan interlock cleared</mark></p>'
132+
)
133+
assert notification._content[1].tags == ["scan_interlock"]
117134
mock_client.queue.remove_queue_lock.assert_called_with(
118135
queue="primary", lock_id="ScanInterlockActor"
119136
)
137+
138+
def test_unlock_skips_notify_without_lock(self, actor, mock_client):
139+
mock_client.queue.queue_storage.current_scan_queue = {"primary": MagicMock(locks=[])}
140+
141+
actor._unlock()
142+
143+
mock_client.connector.notify.assert_not_called()
144+
mock_client.queue.remove_queue_lock.assert_not_called()

0 commit comments

Comments
 (0)