feat(alarms): materialise a Part 9 condition for an alarm equipment tag (Phase B WS-3)
This commit is contained in:
@@ -197,6 +197,71 @@ public sealed class Phase7ApplierTests
|
||||
sink.VariableCalls.ShouldContain(("eq-2/Speed", "eq-2", "Speed", "Float", false));
|
||||
}
|
||||
|
||||
/// <summary>Phase B WS-3 — an alarm-bearing equipment tag (<c>Alarm is not null</c>) materialises a
|
||||
/// real OPC UA Part 9 condition node (via the same path scripted alarms use) instead of a value
|
||||
/// variable; a plain tag (<c>Alarm == null</c>) stays a value variable. The alarm tag's condition
|
||||
/// uses the tag's folder-scoped NodeId, the equipment folder as parent, and carries the tag's
|
||||
/// AlarmType/Severity. Proves BOTH branches in one composition.</summary>
|
||||
[Fact]
|
||||
public void MaterialiseEquipmentTags_alarm_bearing_tag_becomes_condition_plain_tag_stays_variable()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||
|
||||
var composition = new Phase7CompositionResult(
|
||||
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
EquipmentTags = new[]
|
||||
{
|
||||
new EquipmentTagPlan("tag-plain", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null),
|
||||
new EquipmentTagPlan("tag-alarm", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false, Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700)),
|
||||
},
|
||||
};
|
||||
|
||||
applier.MaterialiseEquipmentTags(composition);
|
||||
|
||||
// The plain tag drove EnsureVariable at its folder-scoped NodeId, and NOT a condition.
|
||||
var plainNodeId = EquipmentNodeIds.Variable("eq-1", "", "Speed");
|
||||
sink.VariableCalls.ShouldHaveSingleItem().ShouldBe((plainNodeId, "eq-1", "Speed", "Float", true));
|
||||
sink.AlarmConditionCalls.ShouldNotContain(c => c.AlarmNodeId == plainNodeId);
|
||||
|
||||
// The alarm tag drove MaterialiseAlarmCondition (folder-scoped NodeId, equipment parent,
|
||||
// matching display/type/severity) and did NOT drive EnsureVariable.
|
||||
var alarmNodeId = EquipmentNodeIds.Variable("eq-1", "", "OverTemp");
|
||||
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
||||
.ShouldBe((alarmNodeId, "eq-1", "OverTemp", "OffNormalAlarm", 700));
|
||||
sink.VariableCalls.ShouldNotContain(v => v.NodeId == alarmNodeId);
|
||||
}
|
||||
|
||||
/// <summary>Phase B WS-3 — an alarm-bearing equipment tag WITH a FolderPath still gets its
|
||||
/// sub-folder created, and its condition is parented to that sub-folder (not the equipment folder),
|
||||
/// using the folder-scoped NodeId.</summary>
|
||||
[Fact]
|
||||
public void MaterialiseEquipmentTags_alarm_bearing_tag_with_FolderPath_conditions_under_subfolder()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||
|
||||
var composition = new Phase7CompositionResult(
|
||||
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
EquipmentTags = new[]
|
||||
{
|
||||
new EquipmentTagPlan("tag-alarm", "eq-1", "drv", FolderPath: "Diagnostics", Name: "OverTemp", DataType: "Boolean", FullName: "00001", Writable: false, Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 500)),
|
||||
},
|
||||
};
|
||||
|
||||
applier.MaterialiseEquipmentTags(composition);
|
||||
|
||||
// The sub-folder is still created for an alarm tag with a FolderPath.
|
||||
sink.FolderCalls.ShouldHaveSingleItem().ShouldBe(("eq-1/Diagnostics", "eq-1", "Diagnostics"));
|
||||
// Condition is parented to the sub-folder, with the folder-scoped NodeId. No value variable.
|
||||
var alarmNodeId = EquipmentNodeIds.Variable("eq-1", "Diagnostics", "OverTemp");
|
||||
sink.AlarmConditionCalls.ShouldHaveSingleItem()
|
||||
.ShouldBe((alarmNodeId, "eq-1/Diagnostics", "OverTemp", "OffNormalAlarm", 500));
|
||||
sink.VariableCalls.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <summary>Verifies MaterialiseEquipmentVirtualTags creates one Variable per VirtualTag directly
|
||||
/// under its existing equipment folder, with a folder-scoped NodeId (EquipmentId/Name — NOT the
|
||||
/// VirtualTagId or Expression), parent == EquipmentId, displayName == Name, and does NOT re-create
|
||||
|
||||
Reference in New Issue
Block a user