Source code for app.routes.slow_control

from flask import Blueprint, render_template, redirect, url_for, session, jsonify
from flask_login import login_required
from bson.objectid import ObjectId
from datetime import datetime, timedelta, timezone
from app import mongo
from zoneinfo import ZoneInfo
import os

slow_control = Blueprint('slow_control', __name__)

# Pick the timezone your data was written in (set via env APP_TZ on the server)
APP_TZ = ZoneInfo(os.getenv("APP_TZ", "Europe/Amsterdam"))

def _now_local_naive():
    # Compute “now” in your desired TZ, then drop tzinfo to match naive Mongo datetimes
    return datetime.now(APP_TZ).replace(tzinfo=None)

[docs] def make_plot(sensors, plot_title, yaxis_title, hours=672, add_rangeslider=False): # Build simple Plotly payload from the last N hours of data # end = datetime.now()#timezone.utc) #+ timedelta(hours=1) end = _now_local_naive() start = end - timedelta(hours=hours) # Default visible range: last 48 hours (2 days) default_start = end - timedelta(hours=48) projection = {'timestamp': 1} for s in sensors: projection[s] = 1 cursor = mongo.db.slow_control_data.find( {'timestamp': {'$gte': start, '$lte': end}}, projection ).sort('timestamp', 1) docs = list(cursor) traces = [] all_valid_values = [] # Track all valid values for y-axis range calculation for sensor in sensors: x, y = [], [] for doc in docs: ts = doc.get('timestamp') val = doc.get(sensor) if ts is None or val is None: continue # Ensure UTC-aware and serialize as RFC3339 Z (avoids browser TZ shifts) if getattr(ts, 'tzinfo', None) is None: ts = ts.replace(tzinfo=timezone.utc) x.append(ts.isoformat().replace('+00:00', 'Z')) # Filter out disconnected temperature sensors (TT### > 500C) # Add null to create gaps in the plot instead of connecting lines if sensor.startswith('TT') and val > 500: y.append(None) continue if sensor == 'PP401': val = (val*45.4/100.)**2/31.2 # convert power to W y.append(val) # Collect valid values < 1000 for range calculation if val < 1000: all_valid_values.append(val) traces.append({ 'type': 'scatter', 'mode': 'lines', 'name': sensor, 'x': x, 'y': y, 'connectgaps': False # Don't connect lines across null values }) display_end = end + timedelta(minutes=20) xaxis_config = { 'title': '', 'type': 'date', 'range': [default_start.isoformat(), display_end.isoformat()], 'fixedrange': False # Allow zooming } # Add rangeslider only for the first plot if add_rangeslider: xaxis_config['rangeslider'] = { 'visible': True, 'range': [start.isoformat(), display_end.isoformat()], 'yaxis': {'rangemode': 'auto'} # Allow y-axis to adjust } # Calculate y-axis range from valid values (< 1000) yaxis_config = {'title': yaxis_title, 'fixedrange': False} if all_valid_values: y_min = min(all_valid_values) y_max = max(all_valid_values) # Add 5% padding to the range padding = (y_max - y_min) * 0.05 if y_max != y_min else 1 yaxis_config['range'] = [y_min - padding, y_max + padding] layout = { 'title': {'text': plot_title}, 'uirevision': 'manual-zoom', # preserve UI state if layout changes later 'xaxis': xaxis_config, 'yaxis': yaxis_config, 'height': 300, 'margin': {'l': 100, 'r': 175, 't': 40, 'b': 5}, } return {'data': traces, 'layout': layout}
[docs] def build_plot_payload(): # Sensor groups temperature_in_cryostat = ["TT201", "TT202", "TT203", "TT204", "TT205", "TT206", "TT207", "TT401", "TT402", "TT303", "TT304", "TAMB"] pressures = ["PT101", "PT102", "PT103", "PT104", "PT201"] pump = ["TT301","TT302","TT103","TT104","FM101","PP401"] hv = ["HV_PMT_TOP","HV_PMT_BOT","HV_ANO", "HV_GATE", "HV_CAT", "HV_TS", "HV_BS", "I_PMT_TOP", "I_PMT_BOT"] # Plots plot_temp1 = make_plot(temperature_in_cryostat, "Temperature", "Temperature (C)", add_rangeslider=True) plot_pressure1 = make_plot(pressures, "Pressure", "Pressure (bar)", add_rangeslider=True) plot_pump1 = make_plot(pump, "Pump", "T (C) / F (g/min) / P(W)", add_rangeslider=True) plot_hv1 = make_plot(hv, "High Voltage", "HV (V)", add_rangeslider=True) # Latest values (safe handling if no data) latest = mongo.db.slow_control_data.find_one(sort=[('timestamp', -1)]) or {} selected = ['timestamp', 'PT201', 'TT401', 'TT201', 'TT202', 'FM101'] units = ['', 'bar', 'C', 'C', 'C', 'g/min'] latest_values = {} for k, u in zip(selected, units): v = latest.get(k) if k == 'timestamp' and v is not None and hasattr(v, 'isoformat'): v = v.isoformat() v = v.replace('T',' ') latest_values[k] = (v, u) return { 'plot_temp1': plot_temp1, 'plot_pressure1': plot_pressure1, 'plot_pump1': plot_pump1, 'plot_hv1': plot_hv1, 'latest_values': latest_values }
[docs] @slow_control.route('/plot/') @login_required def plot_view(): # Permission check logbook_id = ObjectId(session['logbook']) logbook = mongo.db.logbooks.find_one({"_id": logbook_id})['name'] if logbook != 'xams': return redirect(url_for('main.index')) data = build_plot_payload() return render_template('slow_control_plot.html', **data)
[docs] @slow_control.route('/plot/data/') @login_required def get_plot_data(): # Permission check logbook_id = ObjectId(session['logbook']) logbook = mongo.db.logbooks.find_one({"_id": logbook_id})['name'] if logbook != 'xams': return jsonify({'error': 'No permission'}), 403 data = build_plot_payload() resp = jsonify(data) resp.headers['Cache-Control'] = 'no-store, max-age=0' resp.headers['Pragma'] = 'no-cache' resp.headers['Expires'] = '0' return resp