fix(alarms): native-alarm re-statements no longer duplicate history or hide acked alarms (#656)
- AlarmTransitionKind.ActiveAcknowledged (appended): active AND acked applied as ONE projection. Galaxy ActiveAcked snapshot entries and OpcUaClient inactive->(active,acked) use it; the projector writes 'Activated' only if not already active, 'Acknowledged' only if not acked. - AlarmEventArgs.IsRestatement (optional, default false): set only by Galaxy snapshot entries and OpcUaClient first sight. A flagged Retrigger on an Active+unacked condition with the same message and severity, or a flagged Clear on an inactive condition, writes no /alerts row. - NativeAlarmProjector reports the history rows per transition; ForwardNativeAlarm writes one row per entry. Unflagged sources (FOCAS/MTConnect/AbCip, live Galaxy) are unchanged. - Tests: projector rules each with a must-write control; OpcUaClient transition table; Galaxy feed/driver mapping; Galaxy + OpcUaClient end-to-end through DriverHostActor. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -1415,8 +1415,11 @@ public sealed class GalaxyDriver
|
||||
GalaxyAlarmTransitionKind.Acknowledge => Core.Abstractions.AlarmTransitionKind.Acknowledge,
|
||||
GalaxyAlarmTransitionKind.Clear => Core.Abstractions.AlarmTransitionKind.Clear,
|
||||
GalaxyAlarmTransitionKind.Retrigger => Core.Abstractions.AlarmTransitionKind.Retrigger,
|
||||
GalaxyAlarmTransitionKind.ActiveAcknowledged => Core.Abstractions.AlarmTransitionKind.ActiveAcknowledged,
|
||||
_ => Core.Abstractions.AlarmTransitionKind.Unspecified,
|
||||
});
|
||||
},
|
||||
// #656: a snapshot entry re-states upstream state — history-only signal for the host.
|
||||
IsRestatement: transition.IsRestatement);
|
||||
try
|
||||
{
|
||||
OnAlarmEvent?.Invoke(this, args);
|
||||
|
||||
@@ -10,6 +10,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
/// on the OPC UA Part 9 condition once <c>AlarmEventArgs</c> is extended in
|
||||
/// the client-surface refresh PR (E.7).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>IsRestatement</c> (Gitea #656) is true for an active-alarm SNAPSHOT entry — upstream re-stating its current
|
||||
/// state on (re)connect, not a transition the gateway watched happen. It rides to
|
||||
/// <see cref="AlarmEventArgs.IsRestatement"/> so the host writes no duplicate history row for it.
|
||||
/// </remarks>
|
||||
internal sealed record GalaxyAlarmTransition(
|
||||
string AlarmFullReference,
|
||||
string SourceObjectReference,
|
||||
@@ -23,7 +28,8 @@ internal sealed record GalaxyAlarmTransition(
|
||||
string OperatorUser,
|
||||
string OperatorComment,
|
||||
string Category,
|
||||
string Description);
|
||||
string Description,
|
||||
bool IsRestatement = false);
|
||||
|
||||
/// <summary>Kind of alarm state change observed by <see cref="EventPump"/>.</summary>
|
||||
internal enum GalaxyAlarmTransitionKind
|
||||
@@ -33,4 +39,10 @@ internal enum GalaxyAlarmTransitionKind
|
||||
Acknowledge = 2,
|
||||
Clear = 3,
|
||||
Retrigger = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #656 — the alarm is active AND acknowledged (a snapshot <c>ActiveAcked</c> entry). Maps to
|
||||
/// <see cref="AlarmTransitionKind.ActiveAcknowledged"/>, which the host applies as one projection.
|
||||
/// </summary>
|
||||
ActiveAcknowledged = 5,
|
||||
}
|
||||
|
||||
@@ -172,18 +172,26 @@ internal sealed class GatewayGalaxyAlarmFeed : IGalaxyAlarmFeed
|
||||
/// alarm is surfaced as a transition so the OPC UA Part 9 condition layer sees
|
||||
/// the alarm's present state on (re)connect: an unacknowledged active alarm as
|
||||
/// a <see cref="GalaxyAlarmTransitionKind.Retrigger"/>, an acknowledged one as a
|
||||
/// <see cref="GalaxyAlarmTransitionKind.Acknowledge"/>.
|
||||
/// <see cref="GalaxyAlarmTransitionKind.ActiveAcknowledged"/>, an inactive one as a
|
||||
/// <see cref="GalaxyAlarmTransitionKind.Clear"/>. Every entry is flagged
|
||||
/// <see cref="GalaxyAlarmTransition.IsRestatement"/>.
|
||||
/// <para>Retrigger, not Raise (Gitea #647 review): the host treats a Raise on a condition it already
|
||||
/// shows Active as a re-statement that KEEPS the acknowledgement. A snapshot re-states upstream state and
|
||||
/// the upstream's ack state is authoritative, so an unacked entry must un-ack a host condition still
|
||||
/// holding an ack from before the reconnect.</para>
|
||||
/// <para>ActiveAcknowledged, not Acknowledge (Gitea #656): an Acknowledge keeps the host's prior Active
|
||||
/// state, so an acked active alarm the host did not already show active (a cold start, or one that went
|
||||
/// active while the gateway was unreachable) projected INACTIVE+acked — invisible to clients.</para>
|
||||
/// <para>IsRestatement (Gitea #656): every gateway reconnect replays the snapshot, so without the flag each
|
||||
/// replay wrote a duplicate "Activated" (or "Cleared") history row for an alarm whose node did not change.
|
||||
/// Live <see cref="DispatchTransition"/> events are never flagged — a live Retrigger is a real re-occurrence.</para>
|
||||
/// </summary>
|
||||
private void DispatchSnapshotEntry(ActiveAlarmSnapshot snapshot)
|
||||
{
|
||||
var kind = snapshot.CurrentState switch
|
||||
{
|
||||
AlarmConditionState.Active => GalaxyAlarmTransitionKind.Retrigger,
|
||||
AlarmConditionState.ActiveAcked => GalaxyAlarmTransitionKind.Acknowledge,
|
||||
AlarmConditionState.ActiveAcked => GalaxyAlarmTransitionKind.ActiveAcknowledged,
|
||||
AlarmConditionState.Inactive => GalaxyAlarmTransitionKind.Clear,
|
||||
_ => GalaxyAlarmTransitionKind.Unspecified,
|
||||
};
|
||||
@@ -210,7 +218,8 @@ internal sealed class GatewayGalaxyAlarmFeed : IGalaxyAlarmFeed
|
||||
OperatorUser: snapshot.OperatorUser,
|
||||
OperatorComment: snapshot.OperatorComment,
|
||||
Category: snapshot.Category,
|
||||
Description: snapshot.Description));
|
||||
Description: snapshot.Description,
|
||||
IsRestatement: true));
|
||||
}
|
||||
|
||||
private void DispatchTransition(OnAlarmTransitionEvent body)
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests"/>
|
||||
<!-- Gitea #656: the native-alarm re-statement end-to-end tests drive this driver into the real DriverHostActor. -->
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Runtime.Tests"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1953,8 +1953,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
if (prior == told) return;
|
||||
kinds = [AlarmTransitionKind.Unspecified];
|
||||
}
|
||||
// #656: on first sight (a start or deploy respawn) this instance is RE-STATING upstream's current state,
|
||||
// not reporting a transition it watched — flagged so the host writes no duplicate history row when its
|
||||
// condition already shows exactly this. History only; the host still projects the state.
|
||||
foreach (var kind in kinds)
|
||||
OnAlarmEvent?.Invoke(this, e with { Kind = kind });
|
||||
OnAlarmEvent?.Invoke(this, e with { Kind = kind, IsRestatement = !priorKnown });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2066,7 +2069,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
/// <summary>
|
||||
/// #619 review — the <see cref="AlarmTransitionKind"/> sequence that moves the host's
|
||||
/// <c>NativeAlarmProjector</c> (which knows only kinds: Raise ⇒ active+unacked — or, on an already-active condition, a re-statement keeping the ack (#647) — Acknowledge ⇒ acked,
|
||||
/// Clear ⇒ inactive keeping acked) from <paramref name="prior"/> to <paramref name="target"/>. A single
|
||||
/// ActiveAcknowledged ⇒ active+acked in one step (#656), Clear ⇒ inactive keeping acked) from <paramref name="prior"/> to <paramref name="target"/>. A single
|
||||
/// upstream event can need two (an alarm that went active and was acknowledged between two notifications).
|
||||
/// No state change ⇒ empty (the caller decides whether a message/severity change warrants an Unspecified).
|
||||
/// </summary>
|
||||
@@ -2097,10 +2100,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
{
|
||||
// Active+unacked: a fresh raise, or a re-arm of an active-acked condition.
|
||||
(true, false) => [prior.Active ? AlarmTransitionKind.Retrigger : AlarmTransitionKind.Raise],
|
||||
// Active+acked: raise first when it was inactive (Raise always lands unacked).
|
||||
// Active+acked from inactive: ONE atomic ActiveAcknowledged (Gitea #656) — Raise-then-Acknowledge showed
|
||||
// clients an Active+UNacked intermediate event before the ack.
|
||||
(true, true) => prior.Active
|
||||
? [AlarmTransitionKind.Acknowledge]
|
||||
: [AlarmTransitionKind.Raise, AlarmTransitionKind.Acknowledge],
|
||||
: [AlarmTransitionKind.ActiveAcknowledged],
|
||||
// Inactive+acked: Clear keeps the prior ack, so ack after it when that was unacked.
|
||||
(false, true) => prior.Active
|
||||
? prior.Acked ? [AlarmTransitionKind.Clear] : [AlarmTransitionKind.Clear, AlarmTransitionKind.Acknowledge]
|
||||
|
||||
+2
@@ -29,6 +29,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests"/>
|
||||
<!-- Gitea #656: the native-alarm re-statement end-to-end tests drive this driver into the real DriverHostActor. -->
|
||||
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Runtime.Tests"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user