From 4b07272556f9a192a90b9b968bee263ee3bcaab8 Mon Sep 17 00:00:00 2001 From: "a.williamson" Date: Fri, 17 Jul 2026 09:27:47 -0500 Subject: [PATCH] Heatmap drill-down to Journal, layout fixes, healthy-plant sim mode (B grade) Heatmap -> Journal drill-down (PrimeBAT only): - clicking an Activations-by-hour/day cell jumps to the Journal filtered to that recurring day-of-week + hour (new query.filterDow/filterHour session props; same local-time bucketing as calc.heatmap) - Journal shows a "Time filter: Thu 12:00-12:59" chip with an X to clear; filter composes with existing filters and is included in CSV export - getJournalPage clamps out-of-range pages when a filter shrinks the set Layout: - filter bar moved below the tab strip so the tabs no longer shift when the bar hides on Overview (tab bar y verified pixel-stable across tabs) - popup titles (AlarmDetail/EventDetail) auto-shrink their font to fit long source paths on one line without colliding with the state/priority chips Alarm health -> B (score ~88): - alarmsim gains a MODE flag: "healthy" (default) generates ~12 activations/hr with an 80/15/5 priority mix and no chatter/standing/fleeting/floods; "chaos" restores the original stress profile - SimHarness gains a SimReset one-shot timer (disabled) that clears all sim tags; probe P3 reports health-grade ground truth for the 8h window - Dashboard onStartup now re-anchors relative range presets (4h/8h/24h/7d) to now on session start - fixes stale Designer-baked startMs/endMs pinning every new session to an old window Co-Authored-By: Claude Fable 5 --- .../session-props/props.json | 10 +- .../Components/HeatmapCell/view.json | 41 +++++- .../views/PrimeControls/Dashboard/view.json | 70 ++++----- .../Popups/AlarmDetail/view.json | 69 +++++---- .../Popups/EventDetail/view.json | 22 ++- .../PrimeControls/Tabs/Analysis/view.json | 14 +- .../PrimeControls/Tabs/Journal/view.json | 99 ++++++++++++- .../client-tags/data.bin | Bin 342 -> 0 bytes .../client-tags/resource.json | 16 --- .../PrimeControls/alarms/code.py | 26 +++- .../ignition/script-python/alarmsim/code.py | 133 ++++++++++-------- .../ignition/script-python/probe/code.py | 18 ++- .../timer/SimReset/handleTimerEvent.py | 2 + .../ignition/timer/SimReset/resource.json | 15 ++ 14 files changed, 377 insertions(+), 158 deletions(-) delete mode 100644 ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/data.bin delete mode 100644 ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json create mode 100644 ignition/gateway/projects/SimHarness/ignition/timer/SimReset/handleTimerEvent.py create mode 100644 ignition/gateway/projects/SimHarness/ignition/timer/SimReset/resource.json diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json index 65147a3..1026701 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/session-props/props.json @@ -3,13 +3,15 @@ "PrimeControls": { "query": { "areas": [], - "endMs": 1784233045923, + "endMs": 0, "priorities": [], "rangePreset": "8h", "refreshToken": 0, "search": "", - "startMs": 1784204245923, - "states": [] + "startMs": 0, + "states": [], + "filterDow": -1, + "filterHour": -1 }, "ui": { "badActorFocus": "", @@ -92,4 +94,4 @@ "theme": "light-cool", "timeZoneId": "America/Chicago" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HeatmapCell/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HeatmapCell/view.json index 5dc1851..06fe669 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HeatmapCell/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Components/HeatmapCell/view.json @@ -2,7 +2,9 @@ "custom": {}, "params": { "count": null, - "max": null + "max": null, + "dow": null, + "hour": null }, "propConfig": { "params.count": { @@ -12,6 +14,14 @@ "params.max": { "paramDirection": "input", "persistent": false + }, + "params.dow": { + "paramDirection": "input", + "persistent": false + }, + "params.hour": { + "paramDirection": "input", + "persistent": false } }, "props": {}, @@ -66,6 +76,20 @@ ], "type": "property" } + }, + "props.style.cursor": { + "binding": { + "config": { + "path": "view.params.dow" + }, + "transforms": [ + { + "code": "\ttry:\n\t\treturn 'pointer' if value is not None else 'default'\n\texcept:\n\t\treturn 'default'", + "type": "script" + } + ], + "type": "property" + } } }, "props": { @@ -77,7 +101,18 @@ "textAlign": "center" } }, - "type": "ia.display.label" + "type": "ia.display.label", + "events": { + "dom": { + "onClick": { + "config": { + "script": "\ttry:\n\t\td = self.view.params.dow\n\t\th = self.view.params.hour\n\t\tif d is None or h is None:\n\t\t\treturn\n\t\tq = self.session.custom.PrimeControls.query\n\t\tq.filterDow = int(d)\n\t\tq.filterHour = int(h)\n\t\tself.session.custom.PrimeControls.ui.selectedTab = 3\n\texcept:\n\t\tpass" + }, + "scope": "G", + "type": "script" + } + } + } } ], "meta": { @@ -91,4 +126,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json index dc8f982..18947ce 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Dashboard/view.json @@ -4,7 +4,7 @@ "system": { "onStartup": { "config": { - "script": "\tq = self.session.custom.PrimeControls.query\n\tif not q.startMs or q.startMs == 0:\n\t\tnow = system.date.toMillis(system.date.now())\n\t\tq.endMs = now\n\t\tq.startMs = now - 8 * 3600 * 1000\n\t\tq.rangePreset = '8h'" + "script": "\tq = self.session.custom.PrimeControls.query\n\tspans = {'4h': 4, '8h': 8, '24h': 24, '7d': 168}\n\tpreset = q.rangePreset\n\tnow = system.date.toMillis(system.date.now())\n\tif preset in spans:\n\t\t# relative presets re-anchor to now on every session start\n\t\tq.endMs = now\n\t\tq.startMs = now - spans[preset] * 3600 * 1000\n\telif not q.startMs or q.startMs == 0:\n\t\tq.endMs = now\n\t\tq.startMs = now - 8 * 3600 * 1000\n\t\tq.rangePreset = '8h'" }, "scope": "G", "type": "script" @@ -59,40 +59,6 @@ }, "type": "ia.display.view" }, - { - "meta": { - "name": "filterBar" - }, - "position": { - "basis": "auto", - "shrink": 0 - }, - "propConfig": { - "props.params.bundle": { - "binding": { - "config": { - "path": "view.custom.bundle" - }, - "type": "property" - } - }, - "position.display": { - "binding": { - "config": { - "expression": "{session.custom.PrimeControls.ui.selectedTab} != 0" - }, - "type": "expr" - } - } - }, - "props": { - "path": "PrimeControls/Shell/FilterBar", - "style": { - "minHeight": "56px" - } - }, - "type": "ia.display.view" - }, { "children": [ { @@ -180,6 +146,40 @@ }, "type": "ia.container.flex" }, + { + "meta": { + "name": "filterBar" + }, + "position": { + "basis": "auto", + "shrink": 0 + }, + "propConfig": { + "props.params.bundle": { + "binding": { + "config": { + "path": "view.custom.bundle" + }, + "type": "property" + } + }, + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.ui.selectedTab} != 0" + }, + "type": "expr" + } + } + }, + "props": { + "path": "PrimeControls/Shell/FilterBar", + "style": { + "minHeight": "56px" + } + }, + "type": "ia.display.view" + }, { "children": [ { diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json index 9addc44..f10031e 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/AlarmDetail/view.json @@ -5,11 +5,11 @@ "custom.detail": { "binding": { "config": { - "expression": "{view.params.source} + \u0027|\u0027 + {session.custom.PrimeControls.query.startMs} + \u0027|\u0027 + {session.custom.PrimeControls.query.endMs}" + "expression": "{view.params.source} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs}" }, "transforms": [ { - "code": "\ttry:\n\t\tsrc \u003d self.view.params.source\n\texcept:\n\t\tsrc \u003d None\n\tif not src:\n\t\treturn None\n\ttry:\n\t\tq \u003d self.session.custom.PrimeControls.query\n\t\treturn PrimeControls.alarms.getSourceDetail(src, q.startMs, q.endMs)\n\texcept:\n\t\treturn None", + "code": "\ttry:\n\t\tsrc = self.view.params.source\n\texcept:\n\t\tsrc = None\n\tif not src:\n\t\treturn None\n\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\treturn PrimeControls.alarms.getSourceDetail(src, q.startMs, q.endMs)\n\texcept:\n\t\treturn None", "type": "script" } ], @@ -47,7 +47,21 @@ }, "transforms": [ { - "code": "\tif value:\n\t\treturn value\n\ttry:\n\t\treturn self.view.params.source or \u0027Alarm Detail\u0027\n\texcept:\n\t\treturn \u0027Alarm Detail\u0027", + "code": "\tif value:\n\t\treturn value\n\ttry:\n\t\treturn self.view.params.source or 'Alarm Detail'\n\texcept:\n\t\treturn 'Alarm Detail'", + "type": "script" + } + ], + "type": "property" + } + }, + "props.style.fontSize": { + "binding": { + "config": { + "path": "this.props.text" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tn = len(value or '')\n\texcept:\n\t\tn = 0\n\tif n <= 34:\n\t\treturn '28px'\n\tsize = int(28.0 * 34 / n)\n\tif size < 12:\n\t\tsize = 12\n\treturn '%dpx' % size", "type": "script" } ], @@ -57,10 +71,17 @@ }, "props": { "style": { - "classes": "PrimeControls/Text/Big" + "classes": "PrimeControls/Text/Big", + "minWidth": "0px", + "whiteSpace": "nowrap" } }, - "type": "ia.display.label" + "type": "ia.display.label", + "position": { + "basis": "auto", + "grow": 1, + "shrink": 1 + } }, { "meta": { @@ -74,7 +95,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn value[\u0027area\u0027] or \u0027\u0027\n\texcept:\n\t\treturn \u0027\u0027", + "code": "\ttry:\n\t\treturn value['area'] or ''\n\texcept:\n\t\treturn ''", "type": "script" } ], @@ -121,7 +142,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['stats']['count'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -153,7 +174,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_tta_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['stats']['avg_tta_ms'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -185,7 +206,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value[\u0027stats\u0027][\u0027avg_active_ms\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.dur(value['stats']['avg_active_ms'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -217,7 +238,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value[\u0027stats\u0027][\u0027fleeting_count\u0027])\n\texcept:\n\t\treturn \u0027--\u0027", + "code": "\ttry:\n\t\treturn PrimeControls.fmt.num(value['stats']['fleeting_count'])\n\texcept:\n\t\treturn '--'", "type": "script" } ], @@ -281,7 +302,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tdaily \u003d list(value[\u0027daily\u0027] or [])\n\texcept:\n\t\tdaily \u003d []\n\tcounts \u003d []\n\tfor d in daily:\n\t\ttry:\n\t\t\tcounts.append(int(d[\u0027count\u0027]))\n\t\texcept:\n\t\t\tcounts.append(0)\n\tif not counts:\n\t\treturn []\n\tm \u003d max(counts)\n\treturn [{\u0027count\u0027: c, \u0027max\u0027: m} for c in counts]", + "code": "\ttry:\n\t\tdaily = list(value['daily'] or [])\n\texcept:\n\t\tdaily = []\n\tcounts = []\n\tfor d in daily:\n\t\ttry:\n\t\t\tcounts.append(int(d['count']))\n\t\texcept:\n\t\t\tcounts.append(0)\n\tif not counts:\n\t\treturn []\n\tm = max(counts)\n\treturn [{'count': c, 'max': m} for c in counts]", "type": "script" } ], @@ -317,7 +338,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tdaily \u003d list(value[\u0027daily\u0027] or [])\n\texcept:\n\t\tdaily \u003d []\n\tif not daily:\n\t\treturn \u0027No activity in range\u0027\n\ttry:\n\t\treturn \u0027Activations per day, %s - %s\u0027 % (PrimeControls.fmt.day_clock(daily[0][\u0027t0\u0027]), PrimeControls.fmt.day_clock(daily[-1][\u0027t0\u0027]))\n\texcept:\n\t\treturn \u0027Activations per day\u0027", + "code": "\ttry:\n\t\tdaily = list(value['daily'] or [])\n\texcept:\n\t\tdaily = []\n\tif not daily:\n\t\treturn 'No activity in range'\n\ttry:\n\t\treturn 'Activations per day, %s - %s' % (PrimeControls.fmt.day_clock(daily[0]['t0']), PrimeControls.fmt.day_clock(daily[-1]['t0']))\n\texcept:\n\t\treturn 'Activations per day'", "type": "script" } ], @@ -382,7 +403,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 0\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 0\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -396,7 +417,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][0]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", + "code": "\ttry:\n\t\tu = value['stats']['top_ack_users'][0]\n\t\treturn '%s (%d)' % (u['user'], u['count'])\n\texcept:\n\t\treturn ''", "type": "script" } ], @@ -428,7 +449,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 1\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 1\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -442,7 +463,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][1]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", + "code": "\ttry:\n\t\tu = value['stats']['top_ack_users'][1]\n\t\treturn '%s (%d)' % (u['user'], u['count'])\n\texcept:\n\t\treturn ''", "type": "script" } ], @@ -474,7 +495,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003e 2\n\texcept:\n\t\treturn False", + "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) > 2\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -488,7 +509,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\tu \u003d value[\u0027stats\u0027][\u0027top_ack_users\u0027][2]\n\t\treturn \u0027%s (%d)\u0027 % (u[\u0027user\u0027], u[\u0027count\u0027])\n\texcept:\n\t\treturn \u0027\u0027", + "code": "\ttry:\n\t\tu = value['stats']['top_ack_users'][2]\n\t\treturn '%s (%d)' % (u['user'], u['count'])\n\texcept:\n\t\treturn ''", "type": "script" } ], @@ -520,7 +541,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\treturn len(value[\u0027stats\u0027][\u0027top_ack_users\u0027]) \u003d\u003d 0\n\texcept:\n\t\treturn True", + "code": "\ttry:\n\t\treturn len(value['stats']['top_ack_users']) == 0\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -600,7 +621,7 @@ }, "transforms": [ { - "code": "\tout \u003d []\n\ttry:\n\t\trows \u003d list(value[\u0027events\u0027] or [])\n\texcept:\n\t\trows \u003d []\n\tfor r in rows[:25]:\n\t\ttry:\n\t\t\tout.append({\u0027time_label\u0027: r[\u0027time_label\u0027], \u0027state_label\u0027: r[\u0027state_label\u0027], \u0027ack_user\u0027: r[\u0027ack_user\u0027] or \u0027\u0027})\n\t\texcept:\n\t\t\tpass\n\treturn out", + "code": "\tout = []\n\ttry:\n\t\trows = list(value['events'] or [])\n\texcept:\n\t\trows = []\n\tfor r in rows[:25]:\n\t\ttry:\n\t\t\tout.append({'time_label': r['time_label'], 'state_label': r['state_label'], 'ack_user': r['ack_user'] or ''})\n\t\texcept:\n\t\t\tpass\n\treturn out", "type": "script" } ], @@ -686,7 +707,7 @@ }, "transforms": [ { - "code": "\tif value is None:\n\t\treturn False\n\ttry:\n\t\treturn not value[\u0027error\u0027]\n\texcept:\n\t\treturn True", + "code": "\tif value is None:\n\t\treturn False\n\ttry:\n\t\treturn not value['error']\n\texcept:\n\t\treturn True", "type": "script" } ], @@ -717,7 +738,7 @@ }, "transforms": [ { - "code": "\tif value is None:\n\t\treturn True\n\ttry:\n\t\treturn bool(value[\u0027error\u0027])\n\texcept:\n\t\treturn False", + "code": "\tif value is None:\n\t\treturn True\n\ttry:\n\t\treturn bool(value['error'])\n\texcept:\n\t\treturn False", "type": "script" } ], @@ -741,7 +762,7 @@ "component": { "onActionPerformed": { "config": { - "script": "\tsystem.perspective.closePopup(\u0027PC_AlarmDetail\u0027)" + "script": "\tsystem.perspective.closePopup('PC_AlarmDetail')" }, "scope": "G", "type": "script" @@ -787,4 +808,4 @@ }, "type": "ia.container.flex" } -} \ No newline at end of file +} diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json index 7fd848c..883a951 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Popups/EventDetail/view.json @@ -40,7 +40,9 @@ "name": "lblTitle" }, "position": { - "grow": 1 + "basis": "auto", + "grow": 1, + "shrink": 1 }, "propConfig": { "props.text": { @@ -56,11 +58,27 @@ ], "type": "property" } + }, + "props.style.fontSize": { + "binding": { + "config": { + "path": "this.props.text" + }, + "transforms": [ + { + "code": "\ttry:\n\t\tn = len(value or '')\n\texcept:\n\t\tn = 0\n\tif n <= 24:\n\t\treturn '28px'\n\tsize = int(28.0 * 24 / n)\n\tif size < 12:\n\t\tsize = 12\n\treturn '%dpx' % size", + "type": "script" + } + ], + "type": "property" + } } }, "props": { "style": { - "classes": "PrimeControls/Text/Big" + "classes": "PrimeControls/Text/Big", + "minWidth": "0px", + "whiteSpace": "nowrap" } }, "type": "ia.display.label" diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json index 3519e36..3995172 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Analysis/view.json @@ -108,7 +108,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][0]][:24]\n\texcept:\n\t\treturn []", + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx, 'dow': 0, 'hour': i} for i, c in enumerate(hm['rows'][0])][:24]\n\texcept:\n\t\treturn []", "type": "script" } ], @@ -174,7 +174,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][1]][:24]\n\texcept:\n\t\treturn []", + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx, 'dow': 1, 'hour': i} for i, c in enumerate(hm['rows'][1])][:24]\n\texcept:\n\t\treturn []", "type": "script" } ], @@ -240,7 +240,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][2]][:24]\n\texcept:\n\t\treturn []", + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx, 'dow': 2, 'hour': i} for i, c in enumerate(hm['rows'][2])][:24]\n\texcept:\n\t\treturn []", "type": "script" } ], @@ -306,7 +306,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][3]][:24]\n\texcept:\n\t\treturn []", + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx, 'dow': 3, 'hour': i} for i, c in enumerate(hm['rows'][3])][:24]\n\texcept:\n\t\treturn []", "type": "script" } ], @@ -372,7 +372,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][4]][:24]\n\texcept:\n\t\treturn []", + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx, 'dow': 4, 'hour': i} for i, c in enumerate(hm['rows'][4])][:24]\n\texcept:\n\t\treturn []", "type": "script" } ], @@ -438,7 +438,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][5]][:24]\n\texcept:\n\t\treturn []", + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx, 'dow': 5, 'hour': i} for i, c in enumerate(hm['rows'][5])][:24]\n\texcept:\n\t\treturn []", "type": "script" } ], @@ -504,7 +504,7 @@ }, "transforms": [ { - "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx} for c in hm['rows'][6]][:24]\n\texcept:\n\t\treturn []", + "code": "\ttry:\n\t\thm = value['heatmap']\n\t\tmx = int(hm['max_count'] or 0)\n\t\treturn [{'count': int(c or 0), 'max': mx, 'dow': 6, 'hour': i} for i, c in enumerate(hm['rows'][6])][:24]\n\texcept:\n\t\treturn []", "type": "script" } ], diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json index 14a4ca2..8120382 100644 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json +++ b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.perspective/views/PrimeControls/Tabs/Journal/view.json @@ -10,11 +10,11 @@ "custom.pageData": { "binding": { "config": { - "expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs} + '|' + {view.custom.page}" + "expression": "{session.custom.PrimeControls.query.refreshToken} + '|' + {session.custom.PrimeControls.query.startMs} + '|' + {session.custom.PrimeControls.query.endMs} + '|' + {session.custom.PrimeControls.query.filterDow} + '|' + {session.custom.PrimeControls.query.filterHour} + '|' + {view.custom.page}" }, "transforms": [ { - "code": "\tsafe = {'rows': [], 'total': 0, 'page': 0, 'page_size': 50, 'truncated': False, 'error': None}\n\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\tif not q.startMs or not q.endMs:\n\t\t\treturn safe\n\t\ttry:\n\t\t\tpage = max(0, int(self.view.custom.page or 0))\n\t\texcept:\n\t\t\tpage = 0\n\t\treturn PrimeControls.alarms.getJournalPage(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search}, page, 50)\n\texcept:\n\t\timport sys\n\t\tsafe['error'] = '%s' % (sys.exc_info()[1],)\n\t\treturn safe", + "code": "\tsafe = {'rows': [], 'total': 0, 'page': 0, 'page_size': 50, 'truncated': False, 'error': None}\n\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\tif not q.startMs or not q.endMs:\n\t\t\treturn safe\n\t\ttry:\n\t\t\tpage = max(0, int(self.view.custom.page or 0))\n\t\texcept:\n\t\t\tpage = 0\n\t\treturn PrimeControls.alarms.getJournalPage(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search, 'dow': q.filterDow, 'hour': q.filterHour}, page, 50)\n\texcept:\n\t\timport sys\n\t\tsafe['error'] = '%s' % (sys.exc_info()[1],)\n\t\treturn safe", "type": "script" } ], @@ -66,6 +66,99 @@ }, "type": "ia.display.label" }, + { + "children": [ + { + "meta": { + "name": "lblTimeFilter" + }, + "position": { + "basis": "auto", + "grow": 0, + "shrink": 0 + }, + "propConfig": { + "props.text": { + "binding": { + "config": { + "struct": { + "dow": "{session.custom.PrimeControls.query.filterDow}", + "hour": "{session.custom.PrimeControls.query.filterHour}" + }, + "waitOnAll": false + }, + "transforms": [ + { + "code": "\tdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']\n\ttry:\n\t\td = int(value['dow'])\n\t\th = int(value['hour'])\n\texcept:\n\t\treturn ''\n\tif d < 0 and h < 0:\n\t\treturn ''\n\tday = days[d] if 0 <= d < 7 else 'Any day'\n\tif h >= 0:\n\t\treturn 'Time filter: %s %02d:00-%02d:59' % (day, h, h)\n\treturn 'Time filter: %s' % day", + "type": "script" + } + ], + "type": "expr-struct" + } + } + }, + "props": { + "style": { + "whiteSpace": "nowrap" + } + }, + "type": "ia.display.label" + }, + { + "events": { + "dom": { + "onClick": { + "config": { + "script": "\tq = self.session.custom.PrimeControls.query\n\tq.filterDow = -1\n\tq.filterHour = -1" + }, + "scope": "G", + "type": "script" + } + } + }, + "meta": { + "name": "icoClearTimeFilter" + }, + "position": { + "basis": "20px", + "grow": 0, + "shrink": 0 + }, + "props": { + "path": "material/close", + "style": { + "cursor": "pointer", + "fontSize": "16px" + } + }, + "type": "ia.display.icon" + } + ], + "meta": { + "name": "chipTimeFilter" + }, + "position": { + "shrink": 0 + }, + "propConfig": { + "position.display": { + "binding": { + "config": { + "expression": "{session.custom.PrimeControls.query.filterDow} > -1 || {session.custom.PrimeControls.query.filterHour} > -1" + }, + "type": "expr" + } + } + }, + "props": { + "alignItems": "center", + "style": { + "classes": "PrimeControls/Chip/Flat", + "gap": "6px" + } + }, + "type": "ia.container.flex" + }, { "meta": { "name": "lblError" @@ -242,7 +335,7 @@ "component": { "onActionPerformed": { "config": { - "script": "\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\tcsv = PrimeControls.alarms.journalCsv(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search})\n\t\tsystem.perspective.download('alarm_journal.csv', csv)\n\texcept:\n\t\tpass" + "script": "\ttry:\n\t\tq = self.session.custom.PrimeControls.query\n\t\tcsv = PrimeControls.alarms.journalCsv(q.startMs, q.endMs, {'priorities': list(q.priorities), 'areas': list(q.areas), 'states': list(q.states), 'search': q.search, 'dow': q.filterDow, 'hour': q.filterHour})\n\t\tsystem.perspective.download('alarm_journal.csv', csv)\n\texcept:\n\t\tpass" }, "scope": "G", "type": "script" diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/data.bin b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/data.bin deleted file mode 100644 index 87e842c294fc1515b8a4c4b7374ea967515bccc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 342 zcmV-c0jd5UiwFP!00000|8-HlPC_vl{VF0C5W&qDhdY_LH02rri5rF(23>f8S_)Qg z>Gif3;{!MvH{<56gNbjUFXZZ1YMiu5^PQgWr>Cb+dk-&zuhH52{pZoiM5k>$dk$QxZi1ta02IB^1tjiHKknt4+U~`(I ztQ(VG(S^*dGIQih0K39ys^*hIIJJLh9;dzFS3mDoYK-duCpx&vVcUvA1Fn^2`ERwR3N^n oLc9mnAO^^Gct6y9OFaso0gW07egKJofS=U-27sOIeBc290FqFjNdN!< diff --git a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json b/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json deleted file mode 100644 index b083298..0000000 --- a/ignition/gateway/projects/PrimeBAT/com.inductiveautomation.vision/client-tags/resource.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scope": "C", - "version": 1, - "restricted": false, - "overridable": true, - "files": [ - "data.bin" - ], - "attributes": { - "lastModificationSignature": "71cfaca87ea3ea9c779495b13fa308d24deb72d86d5d3423e8cd0d949609a7d3", - "lastModification": { - "actor": "admin", - "timestamp": "2026-07-16T20:02:07Z" - } - } -} \ No newline at end of file diff --git a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py index f51522e..04386de 100644 --- a/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py +++ b/ignition/gateway/projects/PrimeBAT/ignition/script-python/PrimeControls/alarms/code.py @@ -188,7 +188,8 @@ def _fetch_shelved(): def _clean_filters(options): - f = {"priorities": [], "areas": [], "states": [], "search": ""} + f = {"priorities": [], "areas": [], "states": [], "search": "", + "dow": -1, "hour": -1} if not options: return f try: @@ -202,6 +203,12 @@ def _clean_filters(options): f["search"] = str(options.get("search") or "") except: pass + # Journal-only heatmap drill-down: day-of-week (0=Mon) + hour, -1 = off. + for k in ("dow", "hour"): + try: + f[k] = int(options.get(k) if options.get(k) is not None else -1) + except: + f[k] = -1 return f @@ -254,9 +261,23 @@ def _journal_rows(start_ms, end_ms, filters, max_rows): areas = set(filters.get("areas") or []) states = set(filters.get("states") or []) search = (filters.get("search") or "").strip().lower() + dow = filters.get("dow", -1) + hour = filters.get("hour", -1) + if dow >= 0 or hour >= 0: + import time for r in rows: if r["is_system"]: continue + # same local-time bucketing as calc.heatmap (tm_wday 0=Mon, tm_hour) + if dow >= 0 or hour >= 0: + try: + lt = time.localtime(r["ts"] / 1000.0) + if dow >= 0 and lt.tm_wday != dow: + continue + if hour >= 0 and lt.tm_hour != hour: + continue + except: + continue parsed = calc.parse_source(r["source"], r["display_path"]) if prios and r["priority"] not in prios: continue @@ -286,6 +307,9 @@ def getJournalPage(startMs, endMs, filters=None, page=0, pageSize=50): int(calc.DEFAULTS["max_events"])) page = max(0, int(page or 0)) page_size = max(1, int(pageSize or 50)) + last_page = max(0, (len(rows) - 1) // page_size) + if page > last_page: + page = last_page start = page * page_size return {"rows": rows[start:start + page_size], "total": len(rows), "page": page, "page_size": page_size, "truncated": truncated, diff --git a/ignition/gateway/projects/SimHarness/ignition/script-python/alarmsim/code.py b/ignition/gateway/projects/SimHarness/ignition/script-python/alarmsim/code.py index 3efb76c..43528e6 100644 --- a/ignition/gateway/projects/SimHarness/ignition/script-python/alarmsim/code.py +++ b/ignition/gateway/projects/SimHarness/ignition/script-python/alarmsim/code.py @@ -1,36 +1,21 @@ -# alarm_simulator.py — Build-a-Thon alarm activity generator (Jython, Ignition 8.3) +# alarmsim - Build-a-Thon alarm activity generator (Jython, Ignition 8.3). +# Driven by the AlarmSimTick gateway timer (5000 ms). # -# Drives the Boolean memory tags in [default]BuildathonSim/ (import -# simulation_tags.json first). Designed to be called every 5 seconds: +# MODE: +# "healthy" - ~12 activations/hr plant-wide, ISA-like 80/15/5 priority mix, +# no chatter / standing / fleeting / floods. Keeps the Alarm +# Health score in the B range. +# "chaos" - original stress profile (floods, chatterers, standing, +# fleeting) for demoing bad-actor analytics. # -# RECOMMENDED — Gateway timer script (survives Designer close): -# 1. Designer > Project Library: create a script named "alarmsim", paste this file. -# 2. Project > Gateway Events > Timer: new script, Delay 5000 ms, -# Fixed Delay, Dedicated thread. Body: alarmsim.tick() -# 3. Save the project. Alarm activity starts immediately and runs forever. -# -# ALTERNATIVE — Designer Script Console (blocks the console while running): -# Paste this whole file, then add at the bottom: -# run_console(minutes=30) -# -# Patterns produced (all timestamps land in the alarm journal): -# - Baseline: random alarms across 5 areas / 3 priorities, active 30 s - 5 min, -# averaging a few activations per minute -# - Chattering: *_Chatter tags re-trigger every 30-60 s (short 5-15 s actives) -# - Standing: *_Standing tags go active on the first tick and never clear -# - Fleeting: *_Fleeting tags are active < 10 s -# - Flood: roughly every 30-60 min (or on demand via force_flood()), one area -# bursts 20-40 activations over 10 minutes -# -# Manual helpers (Script Console, gateway scope via system.util.sendRequest not -# needed — just call from a console if running console mode, or temporarily from -# the timer script): -# alarmsim.force_flood() -> start a flood burst now -# alarmsim.reset() -> clear all sim tags and internal state +# Helpers: alarmsim.force_flood() (chaos only), alarmsim.reset() clears all +# sim tags and internal state. import random import time +MODE = "healthy" + BASE = "[default]BuildathonSim" BASELINE = { @@ -62,12 +47,29 @@ FLEETING = [ "TankFarm/PumpSealFlushLow_Fleeting", ] -# Tuning (per 5 s tick) -BASELINE_PROB = 0.30 # ~3-4 baseline activations/min across the plant -FLEETING_PROB = 0.08 # ~1 fleeting alarm/min -FLOOD_START_PROB = 0.0025 # expected flood every ~30-60 min -FLOOD_DURATION = 600 # 10 minutes -FLOOD_EVENT_PROB = 0.35 # per tick during flood -> ~25 activations / 10 min +# Healthy mode: weighted 80/15/5 Low/Medium/High tag pools (priorities as +# configured in test-data/simulation_tags.json). +HEALTHY_LOW = ["Intake/InletFlowLow", "Intake/SampleTempHigh", "Intake/PowerMonitorAlarm", + "BoilerHouse/StackTempHigh", "BoilerHouse/BlowdownConductivityHigh", + "Packaging/CapperTorqueLow", "Packaging/FillerLevelDeviation", "Packaging/GuardDoorOpen", + "Utilities/CoolingTowerVibration", "Utilities/WaterSoftenerFault", + "TankFarm/VaporRecoveryFault", "TankFarm/T104_PressHigh", "TankFarm/ManifoldLeakDetect"] +HEALTHY_MED = ["Intake/Pump2_Fault", "Intake/ScreenDiffPressHigh", "BoilerHouse/FuelGasPressLow", + "BoilerHouse/EconomizerDPHigh", "Packaging/PrinterInkLow", "Packaging/ConveyorOverload", + "Utilities/GlycolTempHigh", "Utilities/N2PressLow", "TankFarm/T103_TempHigh", + "TankFarm/TransferPumpFault"] +HEALTHY_HIGH = ["Intake/Pump1_Fault", "BoilerHouse/FeedPumpA_Fault", "Packaging/LabelerFault", + "Utilities/ChillerTripped", "TankFarm/T101_LevelHigh"] + +# chaos tuning (per 5 s tick) +BASELINE_PROB = 0.30 +FLEETING_PROB = 0.08 +FLOOD_START_PROB = 0.0025 +FLOOD_DURATION = 600 +FLOOD_EVENT_PROB = 0.35 + +# healthy tuning: ~12 activations/hr -> 12/720 per 5 s tick +HEALTHY_PROB = 12.0 / 720.0 def _path(rel): @@ -78,9 +80,9 @@ def _state(): g = system.util.getGlobals() if "buildathon_alarmsim" not in g: g["buildathon_alarmsim"] = { - "clears": {}, # tag path -> epoch seconds to write False - "chatter_next": {}, # chatter path -> epoch seconds of next re-trigger - "flood": None, # {"area": name, "until": epoch} while flooding + "clears": {}, + "chatter_next": {}, + "flood": None, "standing_set": False, } return g["buildathon_alarmsim"] @@ -92,30 +94,52 @@ def _write(pairs): def tick(): + if MODE == "healthy": + _tick_healthy() + else: + _tick_chaos() + + +def _tick_healthy(): st = _state() now = time.time() writes = [] - - # 1. Standing alarms: activate once, never clear - if not st["standing_set"]: - writes += [(_path(p), True) for p in STANDING] - st["standing_set"] = True - - # 2. Process scheduled clears for path in list(st["clears"].keys()): if now >= st["clears"][path]: writes.append((path, False)) del st["clears"][path] + if random.random() < HEALTHY_PROB: + r = random.random() + if r < 0.80: + rel = random.choice(HEALTHY_LOW) + elif r < 0.95: + rel = random.choice(HEALTHY_MED) + else: + rel = random.choice(HEALTHY_HIGH) + path = _path(rel) + if path not in st["clears"]: + writes.append((path, True)) + st["clears"][path] = now + random.uniform(45, 360) + _write(writes) - # 3. Baseline activity: random tag, active 30 s - 5 min + +def _tick_chaos(): + st = _state() + now = time.time() + writes = [] + if not st["standing_set"]: + writes += [(_path(p), True) for p in STANDING] + st["standing_set"] = True + for path in list(st["clears"].keys()): + if now >= st["clears"][path]: + writes.append((path, False)) + del st["clears"][path] if random.random() < BASELINE_PROB: area = random.choice(list(BASELINE.keys())) path = _path("%s/%s" % (area, random.choice(BASELINE[area]))) if path not in st["clears"]: writes.append((path, True)) st["clears"][path] = now + random.uniform(30, 300) - - # 4. Chattering: re-trigger every 30-60 s, active 5-15 s each time for rel in CHATTER: path = _path(rel) nxt = st["chatter_next"].get(path, 0) @@ -123,15 +147,11 @@ def tick(): writes.append((path, True)) st["clears"][path] = now + random.uniform(5, 15) st["chatter_next"][path] = now + random.uniform(30, 60) - - # 5. Fleeting: active 2-8 s if random.random() < FLEETING_PROB: path = _path(random.choice(FLEETING)) if path not in st["clears"]: writes.append((path, True)) st["clears"][path] = now + random.uniform(2, 8) - - # 6. Flood burst: one area, 15+ activations inside 10 minutes if st["flood"] is None: if random.random() < FLOOD_START_PROB: _start_flood(st, now) @@ -141,11 +161,8 @@ def tick(): if random.random() < FLOOD_EVENT_PROB: area = st["flood"]["area"] path = _path("%s/%s" % (area, random.choice(BASELINE[area]))) - # During a flood, alarms clear fast and may re-trigger, inflating the - # event rate the way a real upset does. Overwrite any pending clear. writes.append((path, True)) st["clears"][path] = now + random.uniform(10, 45) - _write(writes) @@ -156,7 +173,7 @@ def _start_flood(st, now): def force_flood(): - """Start a flood burst immediately (handy for demo/testing).""" + """Start a flood burst immediately (chaos mode only).""" _start_flood(_state(), time.time()) @@ -169,11 +186,3 @@ def reset(): paths += [_path("%s/%s" % (area, n)) for n in names] paths += [_path(p) for p in CHATTER + STANDING + FLEETING] system.tag.writeBlocking(paths, [False] * len(paths)) - - -def run_console(minutes=30): - """Script Console runner: calls tick() every 5 s for `minutes`. Blocks the console.""" - end = time.time() + minutes * 60 - while time.time() < end: - tick() - time.sleep(5) diff --git a/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py b/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py index f4d898e..56b93c4 100644 --- a/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py +++ b/ignition/gateway/projects/SimHarness/ignition/script-python/probe/code.py @@ -98,6 +98,22 @@ def _p2(): return out +def _p3(): + """Health-grade ground truth for the default 8h window.""" + pkg = _load_pc() + end = system.date.toMillis(system.date.now()) + start = end - 8 * 3600 * 1000 + b = pkg.alarms.getDashboardBundle(start, end, None) + return {"grade": b["health"]["grade"], "score": b["health"]["score"], + "subs": [(s["key"], s["score"], s["detail"]) for s in b["health"]["subs"]], + "activations": b["meta"]["activation_count"], + "kpis": {"rate": b["kpis"]["rate_per_hr"]["value"], + "flood_pct": b["kpis"]["flood_pct"]["value"], + "chatter": b["kpis"]["chatter_count"]["value"], + "standing": b["kpis"]["standing_count"]["value"], + "active_now": b["kpis"]["active_now"]["value"]}} + + 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 @@ -311,6 +327,6 @@ def _p0(): def run(): result = {"probe": PROBE_NAME, "ranAt": str(system.date.now())} - result["payload"] = _safe(_p2) + result["payload"] = _safe(_p3) _write(result) system.util.getLogger("probe").info("probe %s written to %s" % (PROBE_NAME, OUT_DIR)) diff --git a/ignition/gateway/projects/SimHarness/ignition/timer/SimReset/handleTimerEvent.py b/ignition/gateway/projects/SimHarness/ignition/timer/SimReset/handleTimerEvent.py new file mode 100644 index 0000000..9bc059a --- /dev/null +++ b/ignition/gateway/projects/SimHarness/ignition/timer/SimReset/handleTimerEvent.py @@ -0,0 +1,2 @@ +def handleTimerEvent(): + alarmsim.reset() diff --git a/ignition/gateway/projects/SimHarness/ignition/timer/SimReset/resource.json b/ignition/gateway/projects/SimHarness/ignition/timer/SimReset/resource.json new file mode 100644 index 0000000..c330570 --- /dev/null +++ b/ignition/gateway/projects/SimHarness/ignition/timer/SimReset/resource.json @@ -0,0 +1,15 @@ +{ + "scope": "G", + "version": 1, + "restricted": false, + "overridable": true, + "files": [ + "handleTimerEvent.py" + ], + "attributes": { + "sharedThread": false, + "delay": 10000, + "fixedDelay": true, + "enabled": false + } +}