Files
lmxopcua/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs
T
Joseph Doherty ddc1f29e3a feat(heidenhain): Part 9 condition class from alarm class/group, ALARM: selectors in the browse picker, unclaimed alarms counted + logged (#839)
- Core: optional AlarmEventArgs.ConditionClass (AlarmConditionClass Base/Process/Maintenance/System/Safety)
  carried by NativeAlarmProjector on AlarmConditionSnapshot and written to ConditionClassId/Name by
  OtOpcUaNodeManager (kept across class-less events, carried across a #641 rebuild).
- Heidenhain: HeidenhainAlarmClasses — HEIDENHAIN DNC-interface class/group names (corroborated by the
  captured iTNC 530 record); class 5 => Safety, group 1/2 => Process, 4/5 => System, else Base.
- DiscoverAsync offers AlarmConditions/ selector leaves (IsAlarm Boolean); browse-commit seeds the alarm object.
- Alarms no selector claims: otopcua.driver.alarms.unmatched counter, /hosts diagnostics, rate-limited Warning.

Refs #839
2026-11-11 03:41:14 -05:00

197 lines
12 KiB
C#

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
/// <summary>
/// Driver capability for alarm events. Optional — only drivers whose backends expose
/// alarm conditions implement this. Currently: Galaxy (MxAccess alarms), FOCAS
/// (CNC alarms), OPC UA Client (A&amp;C events from upstream server), MTConnect
/// (CONDITION data items), AbCip (ALMD/ALMA), Heidenhain (LSV2 controller alarm list, #831).
/// </summary>
public interface IAlarmSource
{
/// <summary>
/// Subscribe to alarm events for a node-set (typically: a folder or equipment subtree).
/// The driver fires <see cref="OnAlarmEvent"/> for every alarm transition.
/// </summary>
/// <param name="sourceNodeIds">The driver node IDs to subscribe to.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A handle identifying the created subscription.</returns>
Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
IReadOnlyList<string> sourceNodeIds,
CancellationToken cancellationToken);
/// <summary>Cancel an alarm subscription returned by <see cref="SubscribeAlarmsAsync"/>.</summary>
/// <param name="handle">The subscription handle returned from <see cref="SubscribeAlarmsAsync"/>.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken);
/// <summary>Acknowledge one or more active alarms by source node ID + condition ID.</summary>
/// <param name="acknowledgements">The batch of alarm acknowledgement requests.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
CancellationToken cancellationToken);
/// <summary>Server-pushed alarm transition (raise / clear / change).</summary>
event EventHandler<AlarmEventArgs>? OnAlarmEvent;
}
/// <summary>Opaque alarm-subscription identity returned by <see cref="IAlarmSource.SubscribeAlarmsAsync"/>.</summary>
public interface IAlarmSubscriptionHandle
{
/// <summary>Driver-internal subscription identifier (for diagnostics + post-mortem).</summary>
string DiagnosticId { get; }
}
/// <summary>One alarm acknowledgement in a batch.</summary>
/// <param name="SourceNodeId">Driver-side identifier for the alarm source.</param>
/// <param name="ConditionId">Stable id correlating raise / ack / clear of the same condition.</param>
/// <param name="Comment">Operator-supplied comment forwarded to the upstream alarm system.</param>
/// <param name="OperatorUser">Authenticated principal performing the ack; null on the raw/non-OPC-UA path.</param>
/// <param name="ApiKeyId">
/// Public id of the API key the OPC UA session authenticated with; null for anonymous / non-key paths.
/// Not a secret. Lets a driver that must acknowledge upstream as a real account (Galaxy, lmxopcua#749)
/// look up the identity mapped to that key.
/// </param>
public sealed record AlarmAcknowledgeRequest(
string SourceNodeId,
string ConditionId,
string? Comment,
string? OperatorUser = null,
string? ApiKeyId = null);
/// <summary>Event payload for <see cref="IAlarmSource.OnAlarmEvent"/>.</summary>
/// <param name="SubscriptionHandle">Subscription this event belongs to.</param>
/// <param name="SourceNodeId">Driver-side identifier for the alarm source.</param>
/// <param name="ConditionId">Stable id correlating raise / ack / clear of the same condition.</param>
/// <param name="AlarmType">Driver-defined alarm type name (e.g. AnalogLimitAlarm.HiHi).</param>
/// <param name="Message">Human-readable alarm description.</param>
/// <param name="Severity">Four-bucket severity ladder.</param>
/// <param name="SourceTimestampUtc">When this transition occurred.</param>
/// <param name="OperatorComment">
/// Operator-supplied comment recorded by the upstream alarm system on Acknowledge
/// transitions. Null on raise / clear, or when the upstream path can't surface
/// the comment (the Galaxy sub-attribute fallback path collapses comments into a
/// single string write — null on that path; the driver-native gateway path
/// populates this).
/// </param>
/// <param name="OriginalRaiseTimestampUtc">
/// When the alarm originally entered the active state. Preserved across
/// Acknowledge transitions so OPC UA Part 9 conditions keep the original raise
/// time in <c>Time</c>. Null when the upstream path doesn't surface it.
/// </param>
/// <param name="AlarmCategory">
/// Upstream alarm taxonomy bucket (e.g. <c>Process</c> / <c>Safety</c> /
/// <c>Diagnostics</c>). Maps to OPC UA <c>ConditionClassName</c> downstream when
/// a class mapping is configured. Null when the upstream path doesn't carry it.
/// </param>
/// <param name="Kind">
/// The alarm transition kind (raise / acknowledge / clear / retrigger). Lets a
/// consumer derive OPC UA Part 9 active/ack state without inferring it from
/// other fields. <see cref="AlarmTransitionKind.Unspecified"/> when the upstream
/// path doesn't surface a distinct kind.
/// </param>
/// <param name="OpcUaSeverity">
/// Optional exact OPC UA severity (1..1000) for a source whose own scale is finer than the four
/// <see cref="AlarmSeverity"/> buckets (MTConnect Warning = 500, Fault = 800). When set it
/// supersedes the bucket on the projected Part 9 condition (clamped to 1..1000); <c>null</c> — every
/// earlier source — keeps the bucket mapping. <see cref="Severity"/> must still carry the nearest
/// bucket for consumers that only read the bucket.
/// </param>
/// <param name="QualityStatusCode">
/// Optional status code of the condition's SOURCE data (Part 9 <c>ConditionType.Quality</c>), for a
/// source that can report "no value for this condition" per alarm rather than per connection (for example
/// MTConnect <c>Unavailable</c>). <c>null</c> keeps the earlier behaviour (projected Good;
/// driver-wide comms loss is annotated separately by the host). An event whose <see cref="Kind"/> is
/// <see cref="AlarmTransitionKind.Unspecified"/> and which carries a quality is a
/// <b>quality-only annotation</b> (<see cref="IsQualityOnly"/>): the host updates (via its Quality-only node path) the condition's
/// Quality without touching its Active/Acked state and publishes no <c>alerts</c> row for it.
/// </param>
/// <param name="IsRestatement">
/// True when the source is RE-STATING upstream's current state rather than reporting a transition it
/// watched happen: a reconnect snapshot entry (Galaxy) or the first event for a condition after a driver start or
/// deploy respawn (OpcUaClient). It governs <b>history only, never node state</b>: the host still projects the
/// state, but writes no <c>alerts</c> row when the re-statement changes nothing the condition shows (a Retrigger on
/// an Active+unacked condition with the same message and severity; a Clear on an already-inactive condition; an
/// Acknowledge on an already-acknowledged condition) — or
/// when a flagged <see cref="AlarmTransitionKind.Unspecified"/> update moves the message/severity of an
/// already-active condition (node updated, one condition event, active/acked kept, no row). AbCip ALMA sets the
/// flag for the last shape (the governing limit changed, no new occurrence). OpcUaClient re-states a
/// first-seen inactive+acked condition as a flagged Clear then a flagged Acknowledge, so a host already showing
/// that writes nothing and a host retaining it Active gets a real "Cleared". <c>false</c> (the default, every source that does not
/// opt in) keeps every transition's row.
/// </param>
/// <param name="ConditionClass">
/// Optional Part 9 <c>ConditionClass</c> of the condition (lmxopcua#839), for a source whose upstream classifies its
/// alarms in a way that maps onto the Part 9 classes with evidence (Heidenhain's error class / group). When set, the
/// host writes it to the condition's <c>ConditionClassId</c> / <c>ConditionClassName</c>; <c>null</c> (every other
/// source) leaves the node's class untouched — <c>BaseConditionClass</c> ("not modelled") unless an earlier event of
/// the same condition set one. A pure annotation: it never moves Active/Acked state and never fires an event on its own.
/// </param>
public sealed record AlarmEventArgs(
IAlarmSubscriptionHandle SubscriptionHandle,
string SourceNodeId,
string ConditionId,
string AlarmType,
string Message,
AlarmSeverity Severity,
DateTime SourceTimestampUtc,
string? OperatorComment = null,
DateTime? OriginalRaiseTimestampUtc = null,
string? AlarmCategory = null,
AlarmTransitionKind Kind = AlarmTransitionKind.Unspecified,
int? OpcUaSeverity = null,
uint? QualityStatusCode = null,
bool IsRestatement = false,
AlarmConditionClass? ConditionClass = null)
{
/// <summary>
/// True when this event only re-annotates the condition's source-data quality — a
/// <see cref="QualityStatusCode"/> with an <see cref="AlarmTransitionKind.Unspecified"/> kind. No
/// alarm-state change happened, so the host must not publish it as an alarm transition.
/// </summary>
public bool IsQualityOnly => Kind == AlarmTransitionKind.Unspecified && QualityStatusCode is not null;
}
/// <summary>Mirrors the <c>NodePermissions</c> alarm-severity enum in <c>docs/v2/acl-design.md</c>.</summary>
public enum AlarmSeverity { Low, Medium, High, Critical }
/// <summary>
/// Kind of alarm state change carried by <see cref="AlarmEventArgs.Kind"/>, letting a
/// consumer derive OPC UA Part 9 active/ack state. Mirrors the driver-side
/// transition-kind enums (e.g. Galaxy's <c>GalaxyAlarmTransitionKind</c>).
/// <see cref="ActiveAcknowledged"/> means "the condition is active AND acknowledged", applied by the
/// host as ONE projection — never Raise-then-Acknowledge, which would show clients an Active+unacked intermediate.
/// <see cref="InactiveUnacknowledged"/> is its mirror: "the condition is inactive AND unacknowledged"
/// (it went active and cleared unacknowledged), applied as ONE projection — never Raise-then-Clear, which showed
/// clients the condition Active again for an instant. The host writes rows only for what actually changed:
/// "Activated" + "Cleared" when the condition was acknowledged (a new occurrence must have happened), "Cleared"
/// alone when it was Active+unacked, none when it already showed inactive+unacked.
/// Members are append-only: their numeric values must not move.
/// </summary>
public enum AlarmTransitionKind { Unspecified = 0, Raise, Acknowledge, Clear, Retrigger, ActiveAcknowledged, InactiveUnacknowledged }
/// <summary>
/// The OPC UA Part 9 condition classes (<c>BaseConditionClassType</c> subtypes, Part 9 §5.9) a driver can assign to a
/// native condition via <see cref="AlarmEventArgs.ConditionClass"/> (lmxopcua#839). Only the classes a driver has
/// evidence for are listed; <see cref="Base"/> is Part 9's "no class modelled". Members are append-only.
/// </summary>
public enum AlarmConditionClass
{
/// <summary><c>BaseConditionClassType</c> — the class is not modelled / not known.</summary>
Base = 0,
/// <summary><c>ProcessConditionClassType</c> — the condition relates to the process the system controls.</summary>
Process,
/// <summary><c>MaintenanceConditionClassType</c> — the condition relates to maintenance.</summary>
Maintenance,
/// <summary><c>SystemConditionClassType</c> — the condition relates to the system itself (hardware, software, comms).</summary>
System,
/// <summary><c>SafetyConditionClassType</c> — the condition relates to safety.</summary>
Safety,
}