Files
BAT/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py
b.peck ecb10b9b7b Phase 1 data layer: calc/fmt/alarms + 31 CPython tests + live gateway P1 smoke
pytest 31/31. In-gateway end-to-end: 559 activations correlated (uuid mode), 3 chatterers,
34 fleeting, 1 flood, health F(22) on the deliberately-bad sim plant. Gate G1 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:08:02 -05:00

397 lines
14 KiB
Python

# PrimeControls.alarms - gateway adapter (the ONLY impure module).
# Journal access via system.alarm.queryJournal/queryStatus exclusively - no SQL,
# no journal name assumptions, every query bounded by the caller's date range.
# Jython 2.7. Bare except is used on defensive paths deliberately: java exceptions
# do not subclass Python Exception and would bypass 'except Exception'.
from __future__ import division, print_function
import sys
from PrimeControls import calc, fmt
def _err_text():
try:
return "%s" % (sys.exc_info()[1],)
except:
return "unknown error"
def _now_ms():
try:
return system.date.toMillis(system.date.now())
except:
import time
return int(time.time() * 1000)
def _ms_of(java_date):
try:
if java_date is None:
return None
return java_date.getTime()
except:
return None
def _norm_event(evt):
try:
source = str(evt.getSource() or "")
state = None
try:
if evt.getAckData() is not None:
state = "ack"
elif evt.getClearedData() is not None:
state = "clear"
elif evt.getActiveData() is not None:
state = "active"
except:
state = None
if state is None:
s = ""
try:
s = str(evt.getState() or "")
except:
pass
if s.startswith("Cleared"):
state = "clear"
elif "Acknowledged" in s and not s.startswith("Active,"):
state = "ack"
else:
state = "active"
ts = None
try:
ts = _ms_of(evt.get("eventTime"))
except:
ts = None
if ts is None:
return None
prio_raw = None
try:
p = evt.getPriority()
try:
prio_raw = int(p.ordinal())
except:
prio_raw = str(p)
except:
prio_raw = None
lvl, name = calc.normalize_priority(prio_raw)
display_path = ""
try:
display_path = str(evt.getDisplayPath() or "")
except:
pass
is_system = source.startswith("evt:") or ":/alm:" not in source
try:
if evt.get("isSystemEvent"):
is_system = True
except:
pass
ack_user = None
try:
u = evt.get("ackUserName")
if u:
ack_user = str(u)
except:
pass
event_id = None
try:
event_id = str(evt.getId())
except:
pass
return {"event_id": event_id, "source": source, "display_path": display_path,
"priority": lvl, "priority_name": name, "state": state, "ts": ts,
"is_system": is_system, "ack_user": ack_user}
except:
return None
def _fetch_journal(start_ms, end_ms, max_events, include_data=False):
"""Returns (rows, dropped, truncated, error)."""
rows = []
dropped = 0
truncated = False
try:
kwargs = {"startDate": int(start_ms), "endDate": int(end_ms)}
result = system.alarm.queryJournal(**kwargs)
for evt in result:
r = _norm_event(evt)
if r is None:
dropped += 1
continue
rows.append(r)
if len(rows) >= max_events:
truncated = True
break
return rows, dropped, truncated, None
except:
return rows, dropped, truncated, _err_text()
def _fetch_status():
out = []
try:
result = system.alarm.queryStatus(state=["ActiveUnacked", "ActiveAcked"])
for evt in result:
try:
source = str(evt.getSource() or "")
if source.startswith("evt:") or ":/alm:" not in source:
continue
active_ms = None
try:
data = evt.getActiveData()
if data is not None:
active_ms = _ms_of(data.get("eventTime"))
except:
active_ms = None
if active_ms is None:
try:
active_ms = _ms_of(evt.get("eventTime"))
except:
pass
prio_raw = None
try:
p = evt.getPriority()
try:
prio_raw = int(p.ordinal())
except:
prio_raw = str(p)
except:
pass
lvl, name = calc.normalize_priority(prio_raw)
unacked = False
try:
unacked = "Unacknowledged" in str(evt.getState() or "")
except:
pass
display_path = ""
try:
display_path = str(evt.getDisplayPath() or "")
except:
pass
out.append({"source": source, "display_path": display_path,
"priority": lvl, "priority_name": name,
"active_ms": active_ms, "unacked": unacked})
except:
continue
except:
pass
return out
def _fetch_shelved():
try:
paths = system.alarm.getShelvedPaths()
return len(list(paths)) if paths is not None else 0
except:
return None
def _clean_filters(options):
f = {"priorities": [], "areas": [], "states": [], "search": ""}
if not options:
return f
try:
for k in ("priorities", "areas", "states"):
v = options.get(k)
if v:
f[k] = [x for x in list(v)]
f["priorities"] = [int(x) for x in f["priorities"]]
f["areas"] = [str(x) for x in f["areas"]]
f["states"] = [str(x) for x in f["states"]]
f["search"] = str(options.get("search") or "")
except:
pass
return f
# ---------------- Perspective entry points ----------------
def getDashboardBundle(startMs, endMs, options=None):
try:
s = int(startMs)
e = int(endMs)
opts = calc.opts_merged(None)
filters = _clean_filters(options)
span = max(e - s, 1)
rows, dropped, truncated, err = _fetch_journal(
s - span, e, int(opts["max_events"]))
active_now = _fetch_status()
shelved = _fetch_shelved()
bundle = calc.build_bundle(rows, active_now, shelved, s, e, _now_ms(),
{"filters": filters})
bundle["meta"]["dropped_rows"] = dropped
bundle["meta"]["truncated"] = truncated
if err:
bundle["meta"]["error"] = err
bundle["insights"] = fmt.insights(bundle)
return bundle
except:
try:
bundle = calc.build_bundle([], [], None, int(startMs or 0),
int(endMs or 0), _now_ms(), None)
except:
bundle = calc.build_bundle([], [], None, 0, 0, 0, None)
bundle["meta"]["error"] = _err_text()
bundle["insights"] = []
return bundle
STATE_LABELS = {"active": "Active", "clear": "Cleared", "ack": "Acked"}
def _journal_rows(start_ms, end_ms, filters, max_rows):
rows, dropped, truncated, err = _fetch_journal(start_ms, end_ms, max_rows)
out = []
prios = set(filters.get("priorities") or [])
areas = set(filters.get("areas") or [])
states = set(filters.get("states") or [])
search = (filters.get("search") or "").strip().lower()
for r in rows:
if r["is_system"]:
continue
parsed = calc.parse_source(r["source"], r["display_path"])
if prios and r["priority"] not in prios:
continue
if areas and parsed["area"] not in areas:
continue
if states:
tag = {"active": "active", "clear": "cleared", "ack": "acked"}[r["state"]]
if tag not in states and not ("unacked" in states and r["state"] == "active"):
continue
if search and search not in (r["source"] + " " + parsed["label"]).lower():
continue
out.append({"id": r["event_id"], "time_ms": r["ts"],
"time_label": fmt.day_clock(r["ts"]),
"source": r["source"], "label": parsed["label"],
"area": parsed["area"], "state": r["state"],
"state_label": STATE_LABELS.get(r["state"], r["state"]),
"priority": r["priority"], "priority_name": r["priority_name"],
"ack_user": r["ack_user"] or ""})
out.sort(key=lambda r: -(r["time_ms"] or 0))
return out, truncated, err
def getJournalPage(startMs, endMs, filters=None, page=0, pageSize=50):
try:
f = _clean_filters(filters)
rows, truncated, err = _journal_rows(int(startMs), int(endMs), f,
int(calc.DEFAULTS["max_events"]))
page = max(0, int(page or 0))
page_size = max(1, int(pageSize or 50))
start = page * page_size
return {"rows": rows[start:start + page_size], "total": len(rows),
"page": page, "page_size": page_size, "truncated": truncated,
"error": err}
except:
return {"rows": [], "total": 0, "page": 0, "page_size": int(pageSize or 50),
"truncated": False, "error": _err_text()}
def journalCsv(startMs, endMs, filters=None, maxRows=10000):
try:
f = _clean_filters(filters)
rows, truncated, err = _journal_rows(int(startMs), int(endMs), f, int(maxRows))
headers = ["time", "state", "priority", "label", "area", "source", "ack_user"]
data = [[r["time_label"], r["state_label"], r["priority_name"], r["label"],
r["area"], r["source"], r["ack_user"]] for r in rows]
ds = system.dataset.toDataSet(headers, data)
return system.dataset.toCSV(ds)
except:
return "error,%s" % _err_text().replace(",", ";")
def getSourceDetail(source, startMs, endMs):
try:
s = int(startMs)
e = int(endMs)
src = str(source or "")
rows, dropped, truncated, err = _fetch_journal(s, e, int(calc.DEFAULTS["max_events"]))
mine = [r for r in rows if r["source"] == src]
instances, _mode = calc.correlate_instances(mine)
parsed = calc.parse_source(src, mine[0]["display_path"] if mine else "")
day_bins = calc.rate_bins(instances, s, e, {"bin_ms": 86400000, "max_bins": 400})
daily = [{"t0": b["t0"], "count": b["count"]} for b in day_bins["bins"]]
ttas = [i["tta_ms"] for i in instances if i.get("tta_ms") is not None]
durs = [i["duration_ms"] for i in instances if i.get("duration_ms") is not None]
fleet = len([i for i in instances
if i.get("duration_ms") is not None
and i["duration_ms"] < calc.DEFAULTS["fleeting_s"] * 1000])
ack_users = {}
for r in mine:
if r["state"] == "ack" and r.get("ack_user"):
ack_users[r["ack_user"]] = ack_users.get(r["ack_user"], 0) + 1
top_ack = [{"user": u, "count": c} for u, c in
sorted(ack_users.items(), key=lambda kv: -kv[1])[:5]]
events = []
for r in sorted(mine, key=lambda r: -r["ts"])[:100]:
events.append({"id": r["event_id"], "time_ms": r["ts"],
"time_label": fmt.day_clock(r["ts"]),
"state_label": STATE_LABELS.get(r["state"], r["state"]),
"priority_name": r["priority_name"],
"ack_user": r["ack_user"] or ""})
return {"label": parsed["label"], "area": parsed["area"], "events": events,
"daily": daily,
"stats": {"count": len([i for i in instances if i["active_ms"] is not None]),
"avg_tta_ms": (sum(ttas) / len(ttas)) if ttas else None,
"avg_active_ms": (sum(durs) / len(durs)) if durs else None,
"fleeting_count": fleet, "top_ack_users": top_ack},
"error": err}
except:
return {"label": str(source or ""), "area": "", "events": [], "daily": [],
"stats": {"count": 0, "avg_tta_ms": None, "avg_active_ms": None,
"fleeting_count": 0, "top_ack_users": []},
"error": _err_text()}
def getEventData(eventId, aroundMs, source):
try:
eid = str(eventId or "")
mid = int(aroundMs)
props = []
seen = {}
try:
result = system.alarm.queryJournal(startDate=mid - 3600000,
endDate=mid + 3600000,
includeData=True)
except:
result = system.alarm.queryJournal(startDate=mid - 3600000,
endDate=mid + 3600000)
for evt in result:
try:
if str(evt.getId()) != eid:
continue
try:
names = evt.getProperties()
except:
names = []
for p in names or []:
try:
n = str(p)
v = evt.get(p)
if n not in seen:
seen[n] = True
props.append({"name": n, "value": str(v)})
except:
continue
except:
continue
props.sort(key=lambda kv: kv["name"])
return {"props": props, "error": None}
except:
return {"props": [], "error": _err_text()}
def getShiftReportText(shiftHours=8, anchorHour=0, options=None):
try:
bounds = fmt.shift_bounds(_now_ms(), int(shiftHours), int(anchorHour))
s, e = bounds["previous"]
bundle = getDashboardBundle(s, e, options)
report = fmt.shift_report(bundle, bounds["label"])
return {"text": report["text"], "start_ms": s, "end_ms": e,
"label": bounds["label"]}
except:
return {"text": "Shift report unavailable: %s" % _err_text(),
"start_ms": 0, "end_ms": 0, "label": ""}