feat(alarms): exact OPC UA severity + per-condition quality on native alarm events (#626)

AlarmEventArgs gains optional OpcUaSeverity (supersedes the four-bucket mapping, clamped
1..1000) and QualityStatusCode (projected onto the condition's Quality). An Unspecified-kind
event carrying a quality is a quality-only annotation: the host updates the condition but
publishes no alerts row. Both default null, so every existing native source is unchanged.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 14:55:46 -04:00
parent e6028678b3
commit cb85a455c9
5 changed files with 140 additions and 6 deletions
@@ -21,7 +21,8 @@ public sealed class NativeAlarmProjector
public AlarmConditionSnapshot Project(string nodeId, AlarmEventArgs e)
{
var prev = _prior.TryGetValue(nodeId, out var p) ? p : (Active: false, Acked: true);
var sev = MapSeverity(e.Severity);
// #626: a source with a finer scale than the four buckets supplies the exact Part 9 severity.
var sev = e.OpcUaSeverity is { } exact ? (ushort)Math.Clamp(exact, 1, 1000) : MapSeverity(e.Severity);
var (active, acked) = e.Kind switch
{
AlarmTransitionKind.Raise or AlarmTransitionKind.Retrigger => (true, false),
@@ -32,7 +33,11 @@ public sealed class NativeAlarmProjector
_prior[nodeId] = (active, acked);
return new AlarmConditionSnapshot(
Active: active, Acknowledged: acked, Confirmed: true, Enabled: true,
Shelving: AlarmShelvingKind.Unshelved, Severity: sev, Message: e.Message);
Shelving: AlarmShelvingKind.Unshelved, Severity: sev, Message: e.Message,
// #626: a per-condition source quality (MTConnect Unavailable) when the driver reports one. Absent
// (every pre-#626 source) ⇒ Good, as before — driver-wide comms loss stays the host's
// out-of-band WriteAlarmQuality annotation (#477), not something the projector infers.
StatusCode: e.QualityStatusCode ?? OpcUaStatus.Good);
}
/// <summary>Clears all tracked per-node state (call on address-space rebuild).</summary>