diff --git a/MainWindow.IedBrowser.cs b/MainWindow.IedBrowser.cs index e94cffacf..952e72d37 100644 --- a/MainWindow.IedBrowser.cs +++ b/MainWindow.IedBrowser.cs @@ -532,9 +532,12 @@ private void RefreshIedBrowserVisibleValues() } else { - row.Value = "Not read"; + row.Value = IedBrowserReadPlan.UnobservedValue( + row.FunctionalConstraint); row.ObservationSource = ""; - row.ObservationTooltip = ""; + row.ObservationTooltip = row.FunctionalConstraint == "CO" + ? "FC CO: command parameters are not scalar MMS Read targets. Use the explicit controlled Signal Testing workflow." + : ""; } continue; } diff --git a/Models/IedBrowserReadPlan.cs b/Models/IedBrowserReadPlan.cs index 0624c2190..f783c2b34 100644 --- a/Models/IedBrowserReadPlan.cs +++ b/Models/IedBrowserReadPlan.cs @@ -31,6 +31,16 @@ public static IedBrowserReadSelectionKind SelectionKind(IedBrowserTreeRow? page) private static readonly HashSet ReadableFc = new( ["ST", "MX", "CF", "DC", "SP", "SG", "SE", "EX"], StringComparer.OrdinalIgnoreCase); + /// + /// FC CO denotes command parameters, not scalar MMS Read targets. + /// Keeping that separate from a genuinely missing read is essential + /// during FAT; no control operation is implied by Browser navigation. + /// + public static string UnobservedValue(string? functionalConstraint) + => string.Equals(functionalConstraint, "CO", StringComparison.OrdinalIgnoreCase) + ? "Control only" + : "Not read"; + public static IReadOnlyList Build( LiveIedModelDiscoveryDocument? model, IedBrowserTreeRow? page, bool readAll, string? selectedDoReference = null) diff --git a/Models/IedBrowserStructuredDetails.cs b/Models/IedBrowserStructuredDetails.cs index d20ceffaa..8e66e57ad 100644 --- a/Models/IedBrowserStructuredDetails.cs +++ b/Models/IedBrowserStructuredDetails.cs @@ -336,7 +336,8 @@ private static IReadOnlyList BuildAt( : TimeChildren(depth + 1, attr.MmsReference, attr.FunctionalConstraint); result.Add(new IedBrowserDetailRow("Data", - $"{name} [{attr.FunctionalConstraint}]", "Not read", depth, + $"{name} [{attr.FunctionalConstraint}]", + IedBrowserReadPlan.UnobservedValue(attr.FunctionalConstraint), depth, $"IEC declaration · {attr.MmsReference} · {attr.TypeSource}") { // A structured intermediate row may have its own observed diff --git a/tests/ARSAS.Tests/M214IedBrowserControlOnlyReadStatusTests.cs b/tests/ARSAS.Tests/M214IedBrowserControlOnlyReadStatusTests.cs new file mode 100644 index 000000000..4c8ed3fd3 --- /dev/null +++ b/tests/ARSAS.Tests/M214IedBrowserControlOnlyReadStatusTests.cs @@ -0,0 +1,48 @@ +using AR.Iec61850.Discovery; +using ArIED61850Tester.Models; + +namespace ARSAS.Tests; + +public sealed class M214IedBrowserControlOnlyReadStatusTests +{ + [Fact] + public void CommandParameters_AreNotDisplayedAsMissingMmsReads() + { + const string objectRef = "LD/CSWI1.Pos"; + var attributes = new[] + { + new LiveIedDataAttributeModel + { + ObjectReference = objectRef, + AttributePath = "Pos.Oper.ctlVal", + MmsReference = "LD/CSWI1$CO$Pos$Oper$ctlVal", + FunctionalConstraint = "CO" + }, + new LiveIedDataAttributeModel + { + ObjectReference = objectRef, + AttributePath = "Pos.stVal", + MmsReference = "LD/CSWI1$ST$Pos$stVal", + FunctionalConstraint = "ST" + } + }; + var roots = IedBrowserStructuredDetails.AttributeRows(attributes, "Pos"); + var control = Assert.Single(roots, row => + row.Name == "Oper [CO]"); + Assert.Equal("Control only", control.Value); + Assert.True(control.HasChildren); + Assert.Equal("Control only", Assert.Single(control.LoadChildren!()).Value); + var status = Assert.Single(roots, row => row.Name == "stVal [ST]"); + Assert.Equal("Not read", status.Value); + } + + [Theory] + [InlineData("CO", "Control only")] + [InlineData("co", "Control only")] + [InlineData("ST", "Not read")] + [InlineData("SG", "Not read")] + [InlineData("CF", "Not read")] + [InlineData("", "Not read")] + public void UnobservedLabel_HasExactFcSemantics(string fc, string expected) + => Assert.Equal(expected, IedBrowserReadPlan.UnobservedValue(fc)); +}