feat(scripts): add the Alarms.CurrentAsync() read accessor for site scripts
MES alarm-status API §5.2 (docs/plans/2026-06-30-mes-alarm-status-api.md,
Phase 1 tasks 2-4). Site `Call` scripts had NO way to read alarm condition
state: the `Alarm` global exists only inside an on-trigger handler and
describes the one alarm that fired, and native mirrored conditions were
reachable only from the Debug View. That gap blocked the CvdReactor
SimpleAlarmStatus/AlarmStatus scripts entirely -- they cannot be written
without it. `Alarms.CurrentAsync()` closes it.
The data was already local: the script runs inside its own Instance Actor's
context, so this is a LOCAL Ask -- the same mechanism attribute reads use, no
cross-cluster hop. A dedicated GetAlarmSnapshotRequest/Response is used rather
than reusing DebugSnapshotRequest, which would materialise every attribute
value on every alarm poll; both are served from the same
BuildAlarmStatesSnapshot(), so the script view and the operator's Debug View
can never disagree.
Deliberate shape decisions:
- NOT scope-prefixed, unlike Attributes. Alarm identity is not a
scope-relative attribute name (computed alarms are keyed by configured
name, native conditions by a source-supplied reference), so prefixing
would hand a composed script a silently truncated list.
- Read-only. Native alarms are a read-only mirror of the source (no
ack-back), so no acknowledge/shelve operation is exposed.
- Placeholder rows are NOT pre-filtered: a caller must be able to tell
"binding configured and quiet" from "binding unknown". The documented
filter is `Active && !IsConfiguredPlaceholder`.
- ScriptAlarm lives in Commons so the runtime accessor and the compile-only
surface project to the SAME type -- a script binding at the design-time
gate binds identically at the site. Condition is the authority for
active/acked/severity, so one filter expression works across computed and
native alarms.
Mirrored on BOTH design-time surfaces. ScriptCompileSurface is covered by the
reflection parity guard (AlarmsAccessor added to its mirror pairs). The Central
UI Test-Run SandboxScriptHost is the third, hand-maintained mirror that the
parity test cannot reach (Central UI does not reference Site Runtime); without
it the design page would false-flag CS1061 on scripts the deploy gate accepts.
It throws a labelled ScriptSandboxException at run time -- there is no central
route to per-instance alarm state, and returning an empty list would read as
"nothing is in alarm", which is worse than an error.
ScriptTrustPolicy needs NO change, and the reason is structural rather than
incidental: the trust boundary is a deny-list over API roots, not an allow-list
of context members. A test pins that no ForbiddenScopes entry prefixes the
Commons script-surface namespace, so a future deny-list entry cannot silently
make ScriptAlarm untouchable.
Tests: 6 accessor cases (Ask contract, full native projection incl. AckTime,
unacked, computed-alarm derivation, placeholder visibility, scope-independence),
2 InstanceActor snapshot cases incl. equality with the Debug View row set, the
full MES script shape compiling against ScriptCompileSurface, 2 trust cases,
and a sandbox diagnose-clean case reading every projected ScriptAlarm field.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Request for the Instance Actor's CURRENT alarm-condition snapshot, backing the
|
||||
/// script-facing <c>Alarms.CurrentAsync()</c> accessor (MES alarm-status API §5.2).
|
||||
///
|
||||
/// <para>
|
||||
/// A dedicated message rather than a reuse of <c>DebugSnapshotRequest</c>: the script
|
||||
/// accessor needs only the alarm rows, and the debug snapshot additionally materialises
|
||||
/// every attribute value on every call. Same local Ask path as
|
||||
/// <see cref="GetAttributeRequest"/> — the script runs against its own Instance Actor, so
|
||||
/// there is no cross-cluster hop.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="CorrelationId">Application-level correlation id echoed on the response.</param>
|
||||
/// <param name="InstanceUniqueName">Unique name of the instance whose alarms are requested.</param>
|
||||
/// <param name="Timestamp">When the request was issued (UTC).</param>
|
||||
public record GetAlarmSnapshotRequest(
|
||||
string CorrelationId,
|
||||
string InstanceUniqueName,
|
||||
DateTimeOffset Timestamp);
|
||||
|
||||
/// <summary>
|
||||
/// The Instance Actor's reply to a <see cref="GetAlarmSnapshotRequest"/> — the same
|
||||
/// enriched alarm rows the Debug View snapshot carries (computed alarms, mirrored native
|
||||
/// conditions, and configured-but-quiet native binding placeholders), projected to
|
||||
/// <c>ScriptAlarm</c> by the accessor rather than by the actor so the message stays a
|
||||
/// plain mirror of the internal state.
|
||||
/// </summary>
|
||||
/// <param name="CorrelationId">Correlation id from the originating request.</param>
|
||||
/// <param name="InstanceUniqueName">Unique name of the instance the snapshot belongs to.</param>
|
||||
/// <param name="Alarms">The instance's current alarm rows.</param>
|
||||
/// <param name="Timestamp">When the snapshot was taken (UTC).</param>
|
||||
public record GetAlarmSnapshotResponse(
|
||||
string CorrelationId,
|
||||
string InstanceUniqueName,
|
||||
IReadOnlyList<AlarmStateChanged> Alarms,
|
||||
DateTimeOffset Timestamp);
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// One alarm condition as seen by a site script through <c>Alarms.CurrentAsync()</c>
|
||||
/// (MES alarm-status API §5.2). A flat, script-friendly projection of the instance's
|
||||
/// retained <c>AlarmStateChanged</c> — computed alarms and mirrored native (OPC UA A&C /
|
||||
/// MxAccess Gateway) conditions alike — so a script can answer "what is currently in
|
||||
/// alarm on this instance?" without knowing the actor-internal message shape.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Read-only snapshot.</b> The values are the instance's state at the moment the
|
||||
/// accessor's Ask was served; nothing here writes back to the source. Native alarms are a
|
||||
/// read-only mirror by design (no ack-back).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Placement in Commons</b> so the SiteRuntime runtime accessor and the ScriptAnalysis
|
||||
/// compile-only surface project to the SAME type — a script that binds against the
|
||||
/// design-time surface therefore also binds at the site. Commons is already in
|
||||
/// <c>ScriptTrustPolicy.DefaultAssemblies</c>, so the type resolves in the script
|
||||
/// compilation without widening the trust reference set.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="Name">
|
||||
/// The alarm's name on the instance — the computed alarm's configured name, or the native
|
||||
/// condition's per-condition source reference.
|
||||
/// </param>
|
||||
/// <param name="SourceReference">Native per-condition key (e.g. "Tank01.Level.HiHi"); empty for computed alarms.</param>
|
||||
/// <param name="NativeSourceCanonicalName">
|
||||
/// Canonical name of the native alarm SOURCE BINDING this condition belongs to
|
||||
/// (e.g. "LeftSideAlarms"); empty for computed alarms. Scripts scope by source with this.
|
||||
/// </param>
|
||||
/// <param name="Active">Whether the condition is currently active (in alarm).</param>
|
||||
/// <param name="Acknowledged">Whether the condition has been acknowledged at the source.</param>
|
||||
/// <param name="Confirmed">Confirmed vs unconfirmed; <c>null</c> when the condition is not confirmable.</param>
|
||||
/// <param name="Shelved">Whether the source has shelved the condition (any shelve sub-state).</param>
|
||||
/// <param name="Suppressed">Whether the source has suppressed the condition.</param>
|
||||
/// <param name="Severity">Severity on the unified 0–1000 scale.</param>
|
||||
/// <param name="Kind">"Computed", "NativeOpcUa" or "NativeMxAccess".</param>
|
||||
/// <param name="Message">Event/per-band operator message; may be empty.</param>
|
||||
/// <param name="AlarmTypeName">Native alarm type (e.g. "AnalogLimitAlarm.HiHi"); empty for computed alarms.</param>
|
||||
/// <param name="Category">Native alarm category/taxonomy; empty for computed alarms.</param>
|
||||
/// <param name="OperatorUser">Operator who acknowledged at the source (display-only); empty otherwise.</param>
|
||||
/// <param name="OperatorComment">Operator comment captured at the source (display-only); empty otherwise.</param>
|
||||
/// <param name="OriginalRaiseTime">When the condition originally became active, if the source reports it.</param>
|
||||
/// <param name="Timestamp">Timestamp of the transition this snapshot row reflects.</param>
|
||||
/// <param name="AckTime">
|
||||
/// When the condition was acknowledged, or <c>null</c> while unacknowledged. See
|
||||
/// <c>AlarmStateChanged.AckTime</c> for provenance — source ack instant where the protocol
|
||||
/// supplies one (OPC UA A&C), DCL observation time of the ack transition otherwise.
|
||||
/// </param>
|
||||
/// <param name="CurrentValue">Current source value (display-only); empty for computed alarms.</param>
|
||||
/// <param name="LimitValue">Limit/threshold value for native limit alarms (display-only); empty otherwise.</param>
|
||||
/// <param name="IsConfiguredPlaceholder">
|
||||
/// True for a placeholder row standing in for a CONFIGURED native source binding that
|
||||
/// currently holds no conditions. Scripts enumerating real alarms should filter these out.
|
||||
/// </param>
|
||||
public sealed record ScriptAlarm(
|
||||
string Name,
|
||||
string SourceReference,
|
||||
string NativeSourceCanonicalName,
|
||||
bool Active,
|
||||
bool Acknowledged,
|
||||
bool? Confirmed,
|
||||
bool Shelved,
|
||||
bool Suppressed,
|
||||
int Severity,
|
||||
string Kind,
|
||||
string Message,
|
||||
string AlarmTypeName,
|
||||
string Category,
|
||||
string OperatorUser,
|
||||
string OperatorComment,
|
||||
DateTimeOffset? OriginalRaiseTime,
|
||||
DateTimeOffset Timestamp,
|
||||
DateTimeOffset? AckTime,
|
||||
string CurrentValue,
|
||||
string LimitValue,
|
||||
bool IsConfiguredPlaceholder);
|
||||
Reference in New Issue
Block a user