feat(alarms): carry transition Kind on AlarmEventArgs; Galaxy populates it (Phase B WS-1)
This commit is contained in:
@@ -71,6 +71,12 @@ public sealed record AlarmAcknowledgeRequest(
|
||||
/// <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>
|
||||
public sealed record AlarmEventArgs(
|
||||
IAlarmSubscriptionHandle SubscriptionHandle,
|
||||
string SourceNodeId,
|
||||
@@ -81,7 +87,15 @@ public sealed record AlarmEventArgs(
|
||||
DateTime SourceTimestampUtc,
|
||||
string? OperatorComment = null,
|
||||
DateTime? OriginalRaiseTimestampUtc = null,
|
||||
string? AlarmCategory = null);
|
||||
string? AlarmCategory = null,
|
||||
AlarmTransitionKind Kind = AlarmTransitionKind.Unspecified);
|
||||
|
||||
/// <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>).
|
||||
/// </summary>
|
||||
public enum AlarmTransitionKind { Unspecified = 0, Raise, Acknowledge, Clear, Retrigger }
|
||||
|
||||
@@ -1153,7 +1153,17 @@ public sealed class GalaxyDriver
|
||||
SourceTimestampUtc: transition.TransitionTimestampUtc,
|
||||
OperatorComment: string.IsNullOrEmpty(transition.OperatorComment) ? null : transition.OperatorComment,
|
||||
OriginalRaiseTimestampUtc: transition.OriginalRaiseTimestampUtc,
|
||||
AlarmCategory: string.IsNullOrEmpty(transition.Category) ? null : transition.Category);
|
||||
AlarmCategory: string.IsNullOrEmpty(transition.Category) ? null : transition.Category,
|
||||
// Fully-qualify the Core.Abstractions enum: this file also imports
|
||||
// MxGateway.Contracts.Proto, which defines a same-named AlarmTransitionKind.
|
||||
Kind: transition.TransitionKind switch
|
||||
{
|
||||
GalaxyAlarmTransitionKind.Raise => Core.Abstractions.AlarmTransitionKind.Raise,
|
||||
GalaxyAlarmTransitionKind.Acknowledge => Core.Abstractions.AlarmTransitionKind.Acknowledge,
|
||||
GalaxyAlarmTransitionKind.Clear => Core.Abstractions.AlarmTransitionKind.Clear,
|
||||
GalaxyAlarmTransitionKind.Retrigger => Core.Abstractions.AlarmTransitionKind.Retrigger,
|
||||
_ => Core.Abstractions.AlarmTransitionKind.Unspecified,
|
||||
});
|
||||
try
|
||||
{
|
||||
OnAlarmEvent?.Invoke(this, args);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Shouldly;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
|
||||
|
||||
public class AlarmEventArgsTests
|
||||
{
|
||||
private static AlarmEventArgs Make(AlarmTransitionKind? kind = null) =>
|
||||
kind is null
|
||||
? new AlarmEventArgs(new FakeHandle(), "Tank1.Hi", "c1", "LimitAlarm.Hi", "msg", AlarmSeverity.High, DateTime.UnixEpoch)
|
||||
: new AlarmEventArgs(new FakeHandle(), "Tank1.Hi", "c1", "LimitAlarm.Hi", "msg", AlarmSeverity.High, DateTime.UnixEpoch, Kind: kind.Value);
|
||||
|
||||
[Fact]
|
||||
public void Kind_defaults_to_Unspecified_so_existing_callers_compile()
|
||||
=> Make().Kind.ShouldBe(AlarmTransitionKind.Unspecified);
|
||||
|
||||
[Fact]
|
||||
public void Kind_round_trips_when_supplied()
|
||||
=> Make(AlarmTransitionKind.Raise).Kind.ShouldBe(AlarmTransitionKind.Raise);
|
||||
|
||||
private sealed class FakeHandle : IAlarmSubscriptionHandle { public string DiagnosticId => "t"; }
|
||||
}
|
||||
@@ -42,6 +42,39 @@ public sealed class GalaxyDriverAlarmSourceTests
|
||||
observed[0].SubscriptionHandle.ShouldBe(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the driver maps each Galaxy transition kind onto the matching
|
||||
/// <see cref="AlarmTransitionKind"/> on the surfaced <see cref="AlarmEventArgs"/>,
|
||||
/// so a Part 9 consumer can derive active/ack state. The Galaxy kind is passed by
|
||||
/// name because <c>GalaxyAlarmTransitionKind</c> is internal to the driver and so
|
||||
/// cannot appear in a public test method signature.
|
||||
/// </summary>
|
||||
/// <param name="galaxyKindName">The <c>GalaxyAlarmTransitionKind</c> member name fed into the alarm feed.</param>
|
||||
/// <param name="expectedKind">The expected <see cref="AlarmTransitionKind"/> on the surfaced event.</param>
|
||||
[Theory]
|
||||
[InlineData(nameof(GalaxyAlarmTransitionKind.Raise), AlarmTransitionKind.Raise)]
|
||||
[InlineData(nameof(GalaxyAlarmTransitionKind.Acknowledge), AlarmTransitionKind.Acknowledge)]
|
||||
[InlineData(nameof(GalaxyAlarmTransitionKind.Clear), AlarmTransitionKind.Clear)]
|
||||
[InlineData(nameof(GalaxyAlarmTransitionKind.Retrigger), AlarmTransitionKind.Retrigger)]
|
||||
[InlineData(nameof(GalaxyAlarmTransitionKind.Unspecified), AlarmTransitionKind.Unspecified)]
|
||||
public async Task Transition_kind_maps_onto_AlarmEventArgs_Kind(
|
||||
string galaxyKindName, AlarmTransitionKind expectedKind)
|
||||
{
|
||||
var galaxyKind = Enum.Parse<GalaxyAlarmTransitionKind>(galaxyKindName);
|
||||
var feed = new FakeAlarmFeed();
|
||||
var ack = new RecordingAcknowledger();
|
||||
using var driver = NewDriver(feed, ack);
|
||||
|
||||
var handle = await driver.SubscribeAlarmsAsync(["Tank01"], CancellationToken.None);
|
||||
var observed = new List<AlarmEventArgs>();
|
||||
driver.OnAlarmEvent += (_, args) => observed.Add(args);
|
||||
|
||||
feed.Emit(NewTransition("Tank01.Level.HiHi", "Tank01", galaxyKind, AlarmSeverity.High));
|
||||
|
||||
observed.ShouldHaveSingleItem();
|
||||
observed[0].Kind.ShouldBe(expectedKind);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that OnAlarmEvent does not fire before any alarm subscription.</summary>
|
||||
[Fact]
|
||||
public void OnAlarmEvent_does_not_fire_before_any_alarm_subscription()
|
||||
|
||||
+5
@@ -6,6 +6,11 @@ using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
|
||||
|
||||
// Core.Abstractions now also defines an AlarmTransitionKind (the AlarmEventArgs surface
|
||||
// kind). This file drives the gateway proto, so pin the bare token to the proto enum to
|
||||
// preserve the existing references unchanged.
|
||||
using AlarmTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user