diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs
index cdb36a264..20a9a4597 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs
@@ -2,6 +2,7 @@ using System.Diagnostics.Metrics;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
@@ -512,7 +513,12 @@ internal sealed class EventPump : IAsyncDisposable
ServerTimestampUtc: DateTime.UtcNow);
}
- var statusCode = ev.Statuses.Count > 0
+ // A failure status row wins (it names why the read failed); otherwise the item's OPC DA quality grades
+ // the value. The gateway fills Statuses on every data change, so keying on "any statuses present" meant
+ // Quality was never read and an off-scan / Bad / Uncertain attribute with an OK status row was
+ // published Good (#866 review). Every gateway path (live OnDataChange, value-cache replay, session
+ // snapshot) carries Quality.
+ var statusCode = ev.Statuses.Count > 0 && !ev.Statuses[0].IsSuccess()
? StatusCodeMap.FromMxStatus(ev.Statuses[0], _logger)
: StatusCodeMap.FromQualityByte((byte)(ev.Quality & 0xFF), _logger);
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/EventPumpQualityTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/EventPumpQualityTests.cs
new file mode 100644
index 000000000..f11b46396
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/EventPumpQualityTests.cs
@@ -0,0 +1,84 @@
+using System.Runtime.CompilerServices;
+using System.Threading.Channels;
+using Google.Protobuf.WellKnownTypes;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.MxGateway.Contracts.Proto;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
+
+///
+/// #866 review: the gateway fills Statuses on every data change, and the pump used the status row
+/// whenever one was present, so the item's OPC DA quality was never read. An off-scan / Bad / Uncertain
+/// attribute carrying an OK status row was published Good. A failure status row still wins.
+///
+public sealed class EventPumpQualityTests
+{
+ private const int WaitMs = 10_000;
+
+ private static MxStatusProxy Ok => new() { Success = 1, Category = MxStatusCategory.Ok };
+
+ private sealed class ChannelSubscriber : IGalaxySubscriber
+ {
+ private readonly Channel _events = Channel.CreateUnbounded();
+
+ public Task> SubscribeBulkAsync(
+ IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
+ => Task.FromResult>([]);
+
+ public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public async IAsyncEnumerable StreamEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ await foreach (var ev in _events.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
+ yield return ev;
+ }
+
+ public ValueTask EmitAsync(int handle, int quality, MxStatusProxy status)
+ {
+ var ev = new MxEvent
+ {
+ Family = MxEventFamily.OnDataChange,
+ ItemHandle = handle,
+ Value = new MxValue { DoubleValue = 1 },
+ Quality = quality,
+ SourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
+ };
+ ev.Statuses.Add(status);
+ return _events.Writer.WriteAsync(ev);
+ }
+ }
+
+ private static async Task PublishedStatusAsync(int quality, MxStatusProxy status)
+ {
+ var subscriber = new ChannelSubscriber();
+ var registry = new SubscriptionRegistry();
+ registry.Register(1, [new TagBinding("Tank01.Level", 7)]);
+ var delivered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ await using var pump = new EventPump(subscriber, registry, channelCapacity: 4, clientName: "Quality");
+ pump.OnDataChange += (_, e) => delivered.TrySetResult(e);
+ pump.Start();
+
+ await subscriber.EmitAsync(7, quality, status);
+ (await Task.WhenAny(delivered.Task, Task.Delay(WaitMs, TestContext.Current.CancellationToken))).ShouldBe(delivered.Task);
+ return (await delivered.Task).Snapshot.StatusCode;
+ }
+
+ [Theory]
+ [InlineData(192, 0x00000000u)] // Good
+ [InlineData(24, 0x808D0000u)] // Bad OutOfService (off scan)
+ [InlineData(8, 0x808A0000u)] // Bad NotConnected
+ [InlineData(68, 0x40900000u)] // Uncertain LastUsableValue
+ public async Task An_ok_status_row_is_graded_by_the_item_quality(int quality, uint expected)
+ => (await PublishedStatusAsync(quality, Ok)).ShouldBe(expected);
+
+ [Fact]
+ public async Task A_failure_status_row_wins_over_a_good_quality()
+ {
+ var failure = new MxStatusProxy { Success = 0, Category = MxStatusCategory.CommunicationError, Detail = 2 };
+ (await PublishedStatusAsync(192, failure)).ShouldBe(StatusCodeMap.BadCommunicationError);
+ }
+}