Note: This branch includes AI-assisted code generation. Treat it with appropriate caution.
This is a Python application that monitors the status of the ISIS beam, experiment updates, and MCR news, sending notifications to designated Microsoft Teams channels. It provides a concurrent monitoring system for real-time facility updates.
- Beam Updates: Monitors the ISIS beam status and sends debounced state-change cards (with severity colour, emoji, current/previous readings and time-in-state) based on configurable thresholds. Correlated multi-target trips are called out in the same card.
- Experiment Updates: Tracks run starts/finishes on any number of instruments, each with its own notify threshold, using each instrument's total µA·h collected (
IN:<NAME>:DAE:TOTALUAMPS): previous-run stats and the new run's target, collection rate and ETA, a warning if data collection stalls while that instrument's beam is on and its DAE is running (and a card when it resumes), and a summary of each experiment (runs, µA·h, duration) when the instrument's RB number changes or goes to "0" (no experiment). - MCR News: Fetches the latest Main Control Room (MCR) news and classifies each update's severity (good/attention/warning) by keyword, with an optional "Open MCR news" link.
- Daily Summary: Sends a per-target uptime/trip/sparkline summary card once a day at a configurable time, with the runs and µA·h of each instrument on that target over the last 24h.
- Microsoft Teams Integration: Sends rich Adaptive Cards directly to configured Teams webhook URLs.
- Signed Webhook (optional): Can also POST every notification as HMAC-signed JSON, e.g. to the optional ISIS Push Site for mobile push notifications (see Signed webhook below).
- Dummy Notifier: Includes a logging-based dummy notifier for testing and development without sending actual webhooks.
- Concurrent Execution: Uses
asyncioto run beam and news monitors concurrently for real-time responsiveness. - Status: With
[PUSH] urlset, the push site's Status page shows each beam with its last 24h, each instrument's run and progress towards its notify threshold (µA·h), the MCR news and the daemon's health.isis-monitor statusprints the same on the host. - Config Editor:
isis-monitor configedits the instruments and notification settings of the running daemon, which validates, saves and restarts itself to apply them.
Ensure you have Python 3.12+ installed. Install the monitor and its dependencies
from the checkout, which also installs the isis-monitor command:
pip install -e .For development and testing, include the development dependencies:
pip install -e '.[dev]'Run the tests, optionally with a coverage report (branch coverage, listing uncovered lines):
pytest
pytest --cov=isis_monitor --cov-branch --cov-report=term-missingThe application reads an INI file. config.ini.example is the annotated example,
with every setting below; start from a copy of it:
cp config.ini.example config.ini- Add your Teams webhook URLs in
[WEBHOOKS]. - Add an
[INSTRUMENT:<NAME>]section for each instrument to monitor (see Instruments).
The whole file is checked when any command starts, and an invalid value stops it with
a Configuration error naming the setting. A blank number (e.g. retention_days =)
means the default, and unknown sections and keys are logged as a warning. Relative
paths are relative to the config file's directory.
Only [DATA] mcr_news_url and at least one [INSTRUMENT:<NAME>] section with a
notify_counts are required; everything else, including whole sections, may be left out.
Times are in seconds unless the key says minutes.
| Section | Key | Default | Allowed values | Meaning |
|---|---|---|---|---|
[DATA] |
mcr_news_url |
none (required) | URL | The MCR news text feed. |
isis_websocket_url |
blank | PVWS WebSocket URL | The PV feed for beams and instruments; blank (logged as a warning) watches neither. | |
mcr_page_url |
blank | URL | Link for an "Open MCR news" button on MCR cards; blank for none. | |
[WEBHOOKS] |
beam_teams_url |
blank | https:// URL |
Teams webhook for beam cards and the daily summary; blank disables it. |
experiment_teams_url |
blank | https:// URL |
Teams webhook for every instrument's run and experiment cards; blank disables it. | |
news_teams_url |
blank | https:// URL |
Teams webhook for MCR news cards; blank disables it. | |
[PUSH] |
url |
blank | http:// or https:// URL, or unix: and a socket's absolute path |
Also POST every notification, signed, here, and the status to this URL plus /status; blank disables it. unix:/run/isis-push-site/ingest.sock posts to /ingest over the push site's socket. Plain http:// to a non-local host is warned about. |
secret_file |
blank | file of 32 to 4096 characters | The shared signing secret; only the daemon reads it, and a world-readable file is warned about. Unset, the daemon reads systemd's push-secret credential (LoadCredential= in its unit), which it then needs. |
|
timeout |
2 | 0.5 to 30 | Seconds before each push attempt is abandoned. | |
[PVS] |
instrument_target |
TS1 |
TS1, TS2, Muons (any case; Muon too) |
The beam_target of instruments that don't set one and that CS:INSTLIST doesn't put on TS1, TS2 or MUON. |
[TIMEOUTS_INTERVALS] |
mcr_poll_interval |
60 | 5 to 86400 | Seconds between MCR news polls. |
beam_reconnect_interval |
5 | 0.5 to 3600 | Seconds before retrying a lost PVWS connection. | |
webhook_timeout |
10 | 1 to 300 | Seconds before a Teams send is abandoned. | |
[DAEMON] |
db_path |
beam_monitor.db |
path | SQLite database of history and state. |
socket_path |
isis_beam_monitor.sock |
path of at most 107 bytes once made absolute | Unix socket the other commands use to reach the daemon. Keep it out of shared directories such as /tmp. |
|
lock_file |
isis_beam_monitor.lock |
path | Lock that keeps to one daemon per config. | |
sample_interval |
60 | 10 to 3600 | Seconds between beam history samples, which is also how often state is saved. | |
status_interval |
5 | 1 to 3600 | Seconds between checks for a changed status to send to [PUSH] url; an unchanged one is sent every 30s. |
|
retention_days |
7 | whole number, 1 to 365 | Days of beam history kept in the database. | |
[LOGGING] |
level |
INFO |
DEBUG, INFO, WARNING, ERROR (any case) |
The daemon's log level. It always logs to stderr (journald under systemd). |
file |
blank | path | Also log to this rotating file (5 MB × 3); blank for none. | |
[NOTIFICATIONS] |
timezone |
Europe/London |
IANA timezone | For card timestamps and summary_time. |
debounce_seconds |
20 | 0 to 3600 | How long a beam state change must last before its card is sent, filtering out flickers. | |
stall_minutes |
15 | above 0, at most 10080 (7 days) | How long the µA·h collected can stay flat, while the instrument's beam is on and its DAE running, before a stall warning. | |
finish_warning_minutes |
15 | 0 to 10080 | How long before a run is expected to reach its notify_counts the "run about to finish" card is sent (0 = once it's reached). |
|
summary_time |
08:00 |
HH:MM, 00:00 to 23:59 |
When the daily beam-uptime summary is sent, in timezone. |
|
[INSTRUMENT:<NAME>] |
notify_counts |
none (required) | positive number, or auto, <positive number> |
The run's target in total µA·h collected; auto sets it per run (see Instruments). |
beam_target |
from CS:INSTLIST |
TS1, TS2, Muons (any case; Muon too) |
The beam reported on run cards and checked before stall warnings. Unset, it's the instrument's target station from CS:INSTLIST, else [PVS] instrument_target. |
|
channel |
experiment |
experiment, instrument (any case) |
The Teams payload channel of its cards: "Experiment Updates", or the instrument's name. |
|
[BEAM:<NAME>] |
pv |
AC:TS1:BEAM:CURR, AC:TS2:BEAM:CURR, AC:MUON:BEAM:CURR |
PV name, different for each beam | The beam's current, in µA. |
boundaries |
TS1 0, 50, 140; TS2 0, 10, 30; Muons 0, 2, 5 |
3 finite numbers, in increasing order (equal allowed) | Power cutoffs in µA: off at or below the first, low below the second, medium below the third, else high. |
<NAME> in [INSTRUMENT:<NAME>] may contain letters, digits, _ and - and is
upper-cased; each instrument appears once. [BEAM:<NAME>] is TS1, TS2 or Muons
(any case; Muon too), once each.
Run notifications for every instrument go to experiment_teams_url, with the
instrument name in the card title (e.g. "PEARL: New run started").
The PVs are derived from the name: progress (total µA·h collected this run) from
IN:<NAME>:DAE:TOTALUAMPS, the run name from IN:<NAME>:DAE:WDTITLE, the run state
from IN:<NAME>:DAE:RUNSTATE_STR, the run number from IN:<NAME>:DAE:RUNNUMBER, its
start time from IN:<NAME>:DAE:STARTTIME and the experiment's RB number from
IN:<NAME>:DAE:_RBNUMBER.
Runs are told apart by their run number, which the DAE moves on (+1) as a run ends. A
run starts once it's been under way (past BEGINNING) for 1s under a new number, and
completes when its number moves on, with the µA·h it had then. Retitling a run, or
setting the next run's title before it begins, sends nothing. An abort keeps the
number: the run is begun again under it (seen by its DAE:STARTTIME), which isn't
another run; if its title changed since it began (e.g. a new temperature), a "Run …
restarted" card says so. After a reconnect, a restart or an outage, PVWS re-sends every
PV in no fixed order, so nothing is decided until both the run number and state have
come, and a run begun meanwhile waits up to 10s for its title; a run begun and ended
unseen meanwhile is still recorded, with its final total if the DAE's counts still
show it.
A daemon started mid-run picks the run up without a card. Stalls are only checked while
a run is under way and the DAE is RUNNING or VETOING, so a paused run, a run-control
wait or a run ended short of its target isn't reported as one.
Downstream flows that route on the payload's channel see "Experiment Updates" unless
an instrument sets channel = instrument.
The daemon also reads each instrument's PV prefix from CS:INSTLIST, as it isn't
always IN:<NAME>: (e.g. PEARL_SETUP is IN:PEARL_5B:). It caches the list, so a
restart starts with the right PVs; an instrument the list doesn't name (probably a
typo) is logged and keeps IN:<NAME>:.
With notify_counts = auto, <number>, each run's target is set when it starts, from the
median of the last 5 completed runs of the same experiment. Very short runs (under 1 µA·h
or a quarter of the typical run, e.g. a mistaken setup) are left out, and <number> is
used until 3 runs remain. The experiment is told apart by its RB number
(IN:<NAME>:DAE:_RBNUMBER): when it changes, including to "0" (no experiment), the
history starts again. The finishing
card shows an auto target as e.g. "60.5 / 60 µA·h (auto)", and status as "≈60".
Sends every notification, on all three channels, as HMAC-signed JSON to one URL as
well as to Teams. It's meant for the optional companion
ISIS Push Site (mobile push notifications,
v2.0.0 or later), but any receiver that checks the signature will work. It also sends
the daemon's status (beams, instruments, MCR news, health), signed the same way, to the
URL plus /status (e.g. /ingest/status), for the push site's Status page: within
[DAEMON] status_interval (5s) of any change, and at least every 30s.
The push site is installed on its own; its README covers setting it up, sharing the
secret with the monitor, and inviting people's phones. With its standard layout, set
url = unix:/run/isis-push-site/ingest.sock and leave secret_file unset: the unit
in deploy/isis-beam-monitor.service gives the daemon the site's secret as its
push-secret credential and puts it in the isis-ingest group, which can reach the
socket. Another receiver needs a secret file of its own, created owner-only, e.g.
(umask 077; openssl rand -hex 32 > push.secret).
Each request has an X-Timestamp header (Unix seconds) and an X-Signature header:
the hex HMAC-SHA256 of <timestamp>.<body> under the secret (the file's contents with
surrounding whitespace, such as the trailing newline, removed). The body is a JSON
object ("v": 1) holding the notification's id, title, text,
severity, emoji, facts, url, url_label, timestamp
(ISO 8601, UTC), topic (TS1/TS2/Muons, the instrument name, MCR
or Summary) and monitor_version. A retry keeps the same id.
The live display is the push site's Status page: each beam with its last 24h, each
instrument's run and progress, the MCR news and the daemon's health, from the status
the daemon pushes to [PUSH] url plus /status. See the push site's README for
setting it up. Without the push site, isis-monitor status shows the same on the host.
isis-monitor daemon path/to/config.ini [-n] [-d] # run the monitor
isis-monitor status path/to/config.ini [--json] # print its status, or its JSON
isis-monitor config path/to/config.ini # edit instruments and notification settings
isis-monitor reconnect path/to/config.ini # reconnect to PVWS and re-read the MCR news
isis-monitor stop path/to/config.ini # shut it down cleanly
isis-monitor --versionThe daemon runs the monitors, sends the notifications and keeps the history; the other
commands talk to it over its [DAEMON] socket_path, so they must run as the daemon's
user with the same config file.
Run isis-monitor config path/to/config.ini to open a numbered menu of the notification settings
(timezone, debounce_seconds, stall_minutes,
finish_warning_minutes, summary_time) and the
instruments (name, notify threshold, beam target, Teams channel). Type a number to edit
that entry (Enter keeps the current value), a to add an instrument, d <number> to
delete one, s to review the changes and save, or q to leave without saving.
On save the daemon validates the new settings (nothing is written if they're invalid, and
you return to the menu to fix them), writes config.ini, and restarts itself in place (same
PID, so it works under systemd or when run by hand).
Things to know:
- The file is rewritten by Python's
configparser, so comments are lost. The previous version is kept asconfig.ini.bak, andconfig.ini.exampledocuments every setting. - If the file changed since the editor opened it (a hand edit, or another editor saving first), the save is refused rather than overwriting those changes; reopen the editor.
- Saving drops unknown keys from instrument sections and writes an explicit
channelfor every instrument.beam_targetis only written when it's set; chooseautoto leave it toCS:INSTLIST. - The daemon's user needs write access to
config.iniand its directory (for the temporary file andconfig.ini.bak); otherwise saving fails with a write error. - Each restart re-sends the beam "Monitor online" cards, as any daemon start does.
-n/--notify_currentis not re-applied on these restarts. - Other settings (webhooks, paths, boundaries, …) are still edited by hand; restart the
daemon afterwards (
sudo systemctl restart isis-beam-monitorunder systemd, orisis-monitor stop path/to/config.iniand start it again). A later save from the editor also applies them, since it re-reads the file first.
-n,--notify_current: Send a notification for the current news immediately on a first start, when there's no saved news yet. Use--no-notify_currentto disable (default behaviour: wait for new news before notifying). Otherwise the daemon compares against the news it last saw, so news posted while it was stopped is still announced.-d,--dummy,--no-dummy: Use a dummy notifier for testing purposes that logs to the console instead of sending actual webhooks.
For example, to try a configuration without sending anything:
isis-monitor daemon config.ini --dummydeploy/isis-beam-monitor.service runs the daemon in a systemd sandbox where its
checkout is read-only. It keeps its config in /etc/isis-beam-monitor (owned by the
daemon's user, so isis-monitor config can rewrite it), its database in
/var/lib/isis-beam-monitor, its socket and lock in /run/isis-beam-monitor and its
log file, if any, in /var/log/isis-beam-monitor; systemd creates the last three. It
restarts after a crash with backoff, but stays stopped after
isis-monitor stop /etc/isis-beam-monitor/config.ini (run as the daemon's user). Its
placeholders are /path/to/ISIS_Beam_Monitor (the checkout, with the dependencies in a
venv at .venv) and MONITOR_USER. Without the push site, delete its
LoadCredential= and SupplementaryGroups= lines. To install it, run from the
checkout as the user the daemon should run as:
- Install the code and the config:
python3 -m venv .venv && .venv/bin/pip install -e . sudo install -d -m 700 -o "$USER" -g "$USER" /etc/isis-beam-monitor install -m 600 config.ini.example /etc/isis-beam-monitor/config.ini
- Fill in
/etc/isis-beam-monitor/config.ini, putting the daemon's files where the unit lets it write:Moving an existing install? Stop it first and copy its database across, or the daemon starts afresh, without its history or the runs and experiments under way (systemd adopts the directory):[DAEMON] db_path = /var/lib/isis-beam-monitor/beam_monitor.db socket_path = /run/isis-beam-monitor/isis_beam_monitor.sock lock_file = /run/isis-beam-monitor/isis_beam_monitor.lock [LOGGING] # Optional: journald has the log too file = /var/log/isis-beam-monitor/monitor.log
sudo install -d -m 700 -o "$USER" -g "$USER" /var/lib/isis-beam-monitor cp -p /old/checkout/beam_monitor.db /var/lib/isis-beam-monitor/
- Install the unit and start it:
sed -e "/^#/!s|/path/to/ISIS_Beam_Monitor|$PWD|g" -e "/^#/!s|MONITOR_USER|$USER|g" \ deploy/isis-beam-monitor.service | sudo tee /etc/systemd/system/isis-beam-monitor.service sudo systemctl daemon-reload sudo systemctl enable --now isis-beam-monitor.service sudo systemctl status isis-beam-monitor.service
The commands (status, config, stop) find the daemon through the paths in that
config, so they work only while it runs under systemd, which creates
/run/isis-beam-monitor.
- Logs: the daemon logs to stderr, so under systemd read them with
journalctl -u isis-beam-monitor(add-fto follow); run by hand, they're on its terminal.[LOGGING] fileadds a log file. Configuration error: …: the named setting is missing or out of range; see the configuration reference.[DAEMON] socket_path … is longer than a Unix socket path can be: Unix socket paths are limited to 107 bytes, and a relativesocket_pathis made absolute from the config file's directory, so a config in a deep directory can exceed it. Set a shorter absolutesocket_path.Lock file already held: another daemon is running with this config (or another config shares itslock_file).No answer from the daemon: check the daemon is running, and that the command runs as the daemon's user (it refuses any other, even root, e.g.sudo -u MONITOR_USER /path/to/ISIS_Beam_Monitor/.venv/bin/isis-monitor status /etc/isis-beam-monitor/config.ini) with the same config file.- No live updates: check the log for PVWS or MCR news errors;
isis-monitor reconnect path/to/config.iniforces a reconnect.
Versions follow Semantic Versioning and are set in isis_monitor/__init__.py (__version__), which is shown at the foot of every Teams card and by isis-monitor --version. The push site is versioned separately in its own repo. To release:
- Move the
[Unreleased]entries inCHANGELOG.mdinto a new## [X.Y.Z] - YYYY-MM-DDsection, noting the push site version it works with, and set__version__toX.Y.Z. - Commit on the release branch, then tag and push both:
git tag -a vX.Y.Z -m vX.Y.Z && git push origin HEAD vX.Y.Z.
The release workflow checks the tag matches __version__, runs the tests and publishes a GitHub release with that section's notes.