Files
BAT/ignition/gateway/projects/SimHarness/ignition/script-python/probe/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

233 lines
8.7 KiB
Python

# probe - gateway-side API ground truth (SimHarness, dev-only, Jython 2.7).
#
# Runs inside the gateway via the ProbeTick timer (enable it, scan, wait <=20s,
# read the output, disable it). Writes JSON to the bind-mounted repo path:
# container: /usr/local/bin/ignition/data/projects/.probe/out.json
# host repo: ignition/gateway/projects/.probe/out.json
#
# Edit PROBE_NAME / add sections as needed; every section is try-wrapped so a
# single API surprise never kills the whole probe.
import json
OUT_DIR = "/usr/local/bin/ignition/data/projects/.probe"
PROBE_NAME = "P1"
def _p1():
"""Exec PrimeBAT's real PrimeControls modules (calc/fmt/alarms) in this scope
and run getDashboardBundle against the live journal - end-to-end data-layer
smoke with zero dev footprint inside PrimeBAT."""
import imp
base = "/usr/local/bin/ignition/data/projects/PrimeBAT/ignition/script-python/PrimeControls/"
pkg = imp.new_module("PrimeControls")
pkg.__path__ = [base]
sys_modules = __import__("sys").modules
sys_modules["PrimeControls"] = pkg
for name in ("calc", "fmt", "alarms"):
m = imp.new_module("PrimeControls." + name)
m.__dict__["system"] = system
code = open(base + name + "/code.py").read()
compiled = compile(code, name, "exec")
# py2 qualified-exec statement form: required because _p1 contains
# closures, which forbid the unqualified exec(...) call form in Jython.
exec compiled in m.__dict__
sys_modules["PrimeControls." + name] = m
setattr(pkg, name, m)
al = pkg.alarms
end = system.date.toMillis(system.date.now())
start = end - 4 * 3600 * 1000
b = al.getDashboardBundle(start, end, None)
page = al.getJournalPage(start, end, None, 0, 5)
shift = al.getShiftReportText()
return {
"meta": {"event_count": b["meta"]["event_count"],
"activation_count": b["meta"]["activation_count"],
"areas": b["meta"]["areas"],
"correlation_mode": b["meta"]["correlation_mode"],
"standing_mode": b["meta"]["standing_mode"],
"error": b["meta"]["error"],
"dropped": b["meta"]["dropped_rows"]},
"health": {"grade": b["health"]["grade"], "score": b["health"]["score"],
"subs": [(s["key"], s["score"]) for s in b["health"]["subs"]]},
"kpis": dict((k, b["kpis"][k]["value"]) for k in b["kpis"]),
"counts": {"chattering": len(b["chattering"]),
"standing": b["standing"]["count"],
"fleeting": b["fleeting"]["total"],
"floods": len(b["floods"]["episodes"]),
"pareto_rows": len(b["pareto"]["rows"]),
"bins": len(b["rate"]["bins"]),
"active_now": len(b["active_now"])},
"insights": [i["text"] for i in b["insights"]],
"journal_page": {"total": page["total"], "rows": len(page["rows"]),
"error": page["error"],
"first": page["rows"][0] if page["rows"] else None},
"shift_label": shift["label"],
"shift_text_head": shift["text"][:400],
}
def _safe(fn):
# Bare except is deliberate: in Jython, JAVA exceptions (e.g. bad kwargs
# into system.* calls) do NOT subclass Python's Exception and would fly
# straight past 'except Exception'.
try:
return {"ok": True, "value": fn()}
except:
import sys
et, ev = sys.exc_info()[0], sys.exc_info()[1]
return {"ok": False, "error": "%s: %s" % (getattr(et, "__name__", et), ev)}
def _write(result):
from java.io import File
File(OUT_DIR).mkdirs()
f = open(OUT_DIR + "/out.json", "w")
try:
f.write(json.dumps(result, indent=1, default=lambda o: repr(o)))
finally:
f.close()
def _prop(evt, names):
out = {}
for n in names:
try:
v = evt.get(n)
out[n] = {"cls": v is not None and v.__class__.__name__ or None,
"repr": repr(v)[:200]}
except Exception, e:
out[n] = {"error": "%s" % e}
return out
def _entry_facts(evt):
d = {}
d["cls"] = evt.__class__.__name__
try:
d["id"] = str(evt.getId())
except Exception, e:
d["id_error"] = "%s" % e
try:
s = evt.getState()
d["state_str"] = str(s)
d["state_cls"] = s is not None and s.__class__.__name__ or None
except Exception, e:
d["state_error"] = "%s" % e
try:
d["source"] = str(evt.getSource())
except Exception, e:
d["source_error"] = "%s" % e
try:
d["displayPath"] = repr(evt.getDisplayPath())
except Exception, e:
d["displayPath_error"] = "%s" % e
try:
p = evt.getPriority()
d["priority_cls"] = p is not None and p.__class__.__name__ or None
d["priority_str"] = str(p)
try:
d["priority_int"] = int(p.ordinal())
except Exception:
try:
d["priority_int"] = int(p)
except Exception:
d["priority_int"] = None
except Exception, e:
d["priority_error"] = "%s" % e
d["props"] = _prop(evt, ["eventTime", "eventType", "eventtype", "ackUser",
"ackUserName", "isSystemEvent", "systemEvent",
"eventValue", "name", "label"])
for getter in ("getActiveData", "getClearedData", "getAckData"):
try:
v = getattr(evt, getter)()
d[getter] = v is not None and repr(v)[:200] or None
except Exception, e:
d[getter] = "ERR %s" % e
try:
from java.util import Date
t = evt.get("eventTime")
if t is not None and isinstance(t, Date):
d["eventTime_ms"] = t.getTime()
except Exception:
pass
return d
def _p0():
result = {}
end = system.date.now()
start = system.date.addHours(end, -8)
def q_basic():
r = system.alarm.queryJournal(startDate=start, endDate=end)
entries = [e for e in r]
by_id = {}
for e in entries:
try:
k = str(e.getId())
except Exception:
k = "?"
by_id.setdefault(k, []).append(e)
multi = {}
n = 0
for k, evts in by_id.items():
if len(evts) > 1 and n < 5:
states = []
for e in evts:
try:
states.append(str(e.getState()))
except Exception:
states.append("?")
multi[k] = states
n += 1
return {
"result_cls": r.__class__.__name__,
"len_works": _safe(lambda: len(r)),
"dataset_attr": hasattr(r, "getDataset"),
"entry_count": len(entries),
"distinct_ids": len(by_id),
"multi_state_id_samples": multi,
"first_entries": [_entry_facts(e) for e in entries[:6]],
"system_rows": [_entry_facts(e) for e in entries
if str(_safe(lambda: str(e.getSource()))["value"]).startswith("evt:")][:3],
}
result["basic"] = _safe(q_basic)
result["no_kwargs_at_all"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end)]))
result["with_journal_name"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
journalName="Journal", startDate=start, endDate=end)]))
result["bad_journal_name"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
journalName="NoSuchJournal", startDate=start, endDate=end)]))
result["long_millis_dates"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=system.date.toMillis(start),
endDate=system.date.toMillis(end))]))
result["include_system_false"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end, includeSystemEvents=False)]))
result["include_system_false_alt"] = _safe(
lambda: len([e for e in system.alarm.queryJournal(
startDate=start, endDate=end, isSystem=False)]))
def q_status():
r = system.alarm.queryStatus(state=["ActiveUnacked", "ActiveAcked"])
entries = [e for e in r]
return {"count": len(entries),
"first_entries": [_entry_facts(e) for e in entries[:5]]}
result["status"] = _safe(q_status)
result["shelved"] = _safe(lambda: repr(system.alarm.getShelvedPaths()))
return result
def run():
result = {"probe": PROBE_NAME,
"ranAt": str(system.date.now())}
result["payload"] = _safe(_p1)
_write(result)
system.util.getLogger("probe").info("probe %s written to %s" % (PROBE_NAME, OUT_DIR))