From fac44187080739db2a6df932ec3d3143003d8ab3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 13:06:37 -0400 Subject: [PATCH] docs(client): document Galaxy fallback operator-comment limitation + pin null behavior Closes backlog #12: Galaxy's AckMsg attribute is WRITE-ONLY and the OPC UA event SelectClause carries no comment field, making OperatorComment unrecoverable on the sub-attribute fallback path. Documents the two concrete reasons in-code and tightens the AlarmEventArgs XML doc; adds pinning test OnAlarmEvent_GalaxyFallback_LeavesOperatorCommentNull. --- .../Models/AlarmEventArgs.cs | 10 ++- .../OpcUaClientService.cs | 9 +++ .../OpcUaClientServiceTests.cs | 76 +++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Models/AlarmEventArgs.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Models/AlarmEventArgs.cs index b47e0426..e73e7337 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Models/AlarmEventArgs.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Models/AlarmEventArgs.cs @@ -81,9 +81,13 @@ public sealed class AlarmEventArgs : EventArgs /// /// PR E.7 — 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 (sub-attribute fallback path collapses comments - /// into a single string write). + /// Acknowledge transitions. Null on raise / clear events, and intentionally null + /// on the Galaxy OPC UA sub-attribute fallback path for two unrecoverable reasons: + /// (a) Galaxy's AckMsg attribute is WRITE-ONLY — no readable attribute + /// exposes the last operator comment — and (b) the OPC UA event SelectClause + /// carries no comment field. The operator comment is only available via the + /// gateway's native alarm feed (GalaxyAlarmTransition.OperatorComment + /// in the Galaxy driver path). /// public string? OperatorComment { get; } diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs index 28d9870c..255ce078 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs @@ -814,6 +814,15 @@ public sealed class OpcUaClientService : IOpcUaClientService // Other supplemental read failure; deliver event with defaults } + // OperatorComment is intentionally left null (default) here. + // Two concrete reasons make it unrecoverable in this Galaxy sub-attribute fallback path: + // (a) Galaxy's AckMsg attribute is WRITE-ONLY — there is no readable attribute that + // exposes the last operator comment, so no supplemental read can recover it. + // (b) The OPC UA event SelectClause (CreateAlarmEventFilter) carries no comment field; + // the OPC UA event notification itself never delivers a comment string. + // The operator comment is only available via the gateway's native alarm feed + // (the Galaxy driver path, GalaxyAlarmTransition.OperatorComment), not this + // OpcUaClient sub-attribute fallback. AlarmEvent?.Invoke(this, new AlarmEventArgs( sourceName, conditionName, severity, capturedMessage, retain, activeState, ackedState, time, eventId, capturedConditionNodeId)); diff --git a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/OpcUaClientServiceTests.cs b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/OpcUaClientServiceTests.cs index 5f4d3d07..8c0fc1f8 100644 --- a/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/OpcUaClientServiceTests.cs +++ b/tests/Client/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/OpcUaClientServiceTests.cs @@ -1251,6 +1251,82 @@ public class OpcUaClientServiceTests : IDisposable received.Message.ShouldBe("Fallback message"); // from DescAttrName read } + /// + /// Pins the accepted limitation that OperatorComment is always null on the Galaxy + /// sub-attribute fallback path. Two concrete reasons make it unrecoverable: + /// (a) Galaxy's AckMsg attribute is WRITE-ONLY — no readable attribute exposes the + /// last operator comment — and (b) the OPC UA event SelectClause carries no comment + /// field. The operator comment is only available via the gateway's native alarm feed + /// (GalaxyAlarmTransition.OperatorComment in the Galaxy driver path, not here). + /// + [Fact] + public async Task OnAlarmEvent_GalaxyFallback_LeavesOperatorCommentNull() + { + var fakeSub = new FakeSubscriptionAdapter(); + var session = new FakeSessionAdapter + { + NextSubscription = fakeSub, + ReadResponseFunc = nodeId => + { + var key = nodeId.ToString(); + if (key.EndsWith(".InAlarm")) + return new DataValue(new Variant(true), StatusCodes.Good); + if (key.EndsWith(".Acked")) + return new DataValue(new Variant(true), StatusCodes.Good); + if (key.EndsWith(".TimeAlarmOn")) + return new DataValue(new Variant(new DateTime(2026, 1, 1, 12, 0, 0)), StatusCodes.Good); + if (key.EndsWith(".DescAttrName")) + return new DataValue(new Variant("Ack message"), StatusCodes.Good); + return new DataValue(StatusCodes.BadNodeIdUnknown); + } + }; + _sessionFactory.EnqueueSession(session); + await _service.ConnectAsync(ValidSettings()); + + AlarmEventArgs? received = null; + var raised = new TaskCompletionSource(); + _service.AlarmEvent += (_, e) => + { + received = e; + raised.TrySetResult(); + }; + + await _service.SubscribeAlarmsAsync(); + + var handle = fakeSub.ActiveHandles.First(); + // AckedState/Id (8) and ActiveState/Id (9) are null, triggering the Galaxy + // sub-attribute fallback; SourceNode (12) is set so conditionNodeId is populated. + var fields = new EventFieldList + { + EventFields = + [ + new Variant(new byte[] { 4, 5, 6 }), // 0: EventId + new Variant(ObjectTypeIds.AlarmConditionType), // 1: EventType + new Variant("Source2"), // 2: SourceName + new Variant(DateTime.MinValue), // 3: Time + new Variant(new LocalizedText("Ack")), // 4: Message + new Variant((ushort)200), // 5: Severity + new Variant("CondName2"), // 6: ConditionName + new Variant(true), // 7: Retain + Variant.Null, // 8: AckedState/Id — missing + Variant.Null, // 9: ActiveState/Id — missing + new Variant(true), // 10: EnabledState/Id + new Variant(false), // 11: SuppressedOrShelved + new Variant("ns=2;s=ConditionId2") // 12: SourceNode + ] + }; + fakeSub.SimulateEvent(handle, fields); + + // The fallback runs on a background Task.Run continuation — wait briefly for it. + await Task.WhenAny(raised.Task, Task.Delay(500)); + + received.ShouldNotBeNull(); + // OperatorComment must be null: AckMsg is WRITE-ONLY in Galaxy and the OPC UA + // event SelectClause carries no comment field — both reasons are unrecoverable + // on this client sub-attribute fallback path. + received!.OperatorComment.ShouldBeNull(); + } + // --- Failover tests --- ///