Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions Models/RcbInspectionEvidence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ public IReadOnlyList<RcbInspectionProperty> Properties(RcbExportRow row, bool co
!string.IsNullOrWhiteSpace(live?.ReportId) ? source : ModelSource),
new("DataSet reference", Show(row.DataSetReference),
row.IsSourceBacked ? "SCL / effective read-only DataSet binding" : "Discovery / effective read-only DataSet binding"),
Field("Enabled / RptEna", live?.Enabled, string.Empty, source),
Field("Reservation", live?.Reservation, string.Empty, source),
Field("Reserve time (s)", live?.ReserveTime, string.Empty, source),
StateField("Enabled / RptEna", live?.Enabled, Model.Enabled, connected, source),
StateField("Reservation", live?.Reservation, Model.Reservation, connected, source),
StateField("Reserve time (s)", live?.ReserveTime, Model.ReserveTime, connected, source),
Field("Owner", live?.Owner, string.Empty, source),
Field("Trigger options", live?.TriggerOptions, Model.TriggerOptions, source),
Field("Optional fields", live?.OptionalFields, Model.OptionalFields, source),
Expand All @@ -110,6 +110,35 @@ public IReadOnlyList<RcbInspectionProperty> Properties(RcbExportRow row, bool co
return list;
}

private RcbInspectionProperty StateField(
string name, string? checkedValue, string? discoveryValue,
bool connected, string liveSource)
{
if (UsableObservation(checkedValue))
return new(name, checkedValue!.Trim(), liveSource);

// A successful connect/discovery may already have observed RptEna,
// Resv, and ResvTms. It is historical metadata, NOT the present
// RCB reservation/enable state and never proof of local ownership.
// Source-backed SCL declarations do not contain these observations.
if (connected && ModelSource == "Saved discovery model (not live state)" &&
UsableObservation(discoveryValue))
return new(name, discoveryValue!.Trim(),
"Discovery snapshot (historical; explicit MMS check for current state)");

return new(name, "—", "Current MMS state not verified");
}

private static bool UsableObservation(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return false;
var text = value.Trim();
return text is not ("—" or "-" or "?") &&
!text.Equals("Unknown", StringComparison.OrdinalIgnoreCase) &&
!text.Equals("Not read", StringComparison.OrdinalIgnoreCase) &&
!text.Equals("Unavailable", StringComparison.OrdinalIgnoreCase);
}

private RcbInspectionProperty Field(string name, string? live, string? model, string liveSource)
=> !string.IsNullOrWhiteSpace(live)
? new(name, live.Trim(), liveSource)
Expand Down
93 changes: 93 additions & 0 deletions tests/ARSAS.Tests/M214RcbDiscoveryStateProjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using AR.Iec61850.Discovery;
using AR.Iec61850.Mms;
using ArIED61850Tester.Models;

namespace ARSAS.Tests;

public sealed class M214RcbDiscoveryStateProjectionTests
{
private static readonly RcbExportRow Row = new()
{
Name = "urcb01", Reference = "LD/LLN0.RP.urcb01",
DataSetReference = "LD/LLN0.Signals"
};

private static LiveIedReportControlModel Discovery(
string enabled = "true", string reservation = "false",
string reserveTime = "0") => new()
{
Reference = Row.Reference,
Name = Row.Name,
EnabledState = enabled, ReservationState = reservation,
ReservationTimeSeconds = reserveTime
};

private static RcbInspectionProperty Property(
RcbInspectionEvidence evidence, bool connected, string field)
=> Assert.Single(evidence.Properties(Row, connected),
property => property.Property == field);

[Fact]
public void DiscoveryValuesAppearAsHistorical_NotAsCurrentReadOnlyCheck()
{
var evidence = RcbInspectionEvidence.FromDiscovery(Discovery(), null);
foreach (var (name, value) in new[]
{
("Enabled / RptEna", "true"),
("Reservation", "false"),
("Reserve time (s)", "0")
})
{
var field = Property(evidence, true, name);
Assert.Equal(value, field.Value);
Assert.Contains("historical", field.Source, StringComparison.OrdinalIgnoreCase);
}
Assert.Contains("no current MMS state check", evidence.Summary(true));
}

[Fact]
public void NoLiveStateIsProjectedAfterDisconnect_OrFromSclOnly()
{
var saved = RcbInspectionEvidence.FromDiscovery(Discovery(), null);
Assert.Equal("—", Property(saved, false, "Enabled / RptEna").Value);
Assert.Equal("—", Property(saved, false, "Reservation").Value);
Assert.Equal("—", Property(saved, false, "Reserve time (s)").Value);

var fromScl = RcbInspectionEvidence.FromSource("Rpt01", null);
Assert.Equal("—", Property(fromScl, true, "Enabled / RptEna").Value);
Assert.Equal("—", Property(fromScl, true, "Reservation").Value);
}

[Fact]
public void UnknownSentinelsNeverClaimRcbDiscoveryState()
{
var evidence = RcbInspectionEvidence.FromDiscovery(
Discovery("Unknown", "Not read", "—"), null);
Assert.Equal("—", Property(evidence, true, "Enabled / RptEna").Value);
Assert.Equal("—", Property(evidence, true, "Reservation").Value);
Assert.Equal("—", Property(evidence, true, "Reserve time (s)").Value);
}

[Fact]
public void ExplicitSnapshotWinsOverContradictoryDiscovery()
{
var snapshot = new MmsRcbAvailabilitySnapshot
{
EnabledState = "false", ReservationState = "true",
ReservationTimeSeconds = "12"
};
var evidence = RcbInspectionEvidence.FromDiscovery(
Discovery("true", "false", "0"), snapshot);
foreach (var (name, expected) in new[]
{
("Enabled / RptEna", "false"),
("Reservation", "true"),
("Reserve time (s)", "12")
})
{
var field = Property(evidence, true, name);
Assert.Equal(expected, field.Value);
Assert.Equal("Read-only MMS availability snapshot", field.Source);
}
}
}
Loading