4624141a5d
tag gate, and fix three silently-broken OpcUaClient integration tests
Continues working through deferment.md's remaining items.
SKIPPED TESTS (13 -> 0 for this reason). The skip said "dark until Batch 4";
Batch 4 shipped as v3.0 and RETIRED the equipment-tag path rather than lighting
it, so every one of them was waiting on something that had already happened and
gone the other way. Resolved individually rather than in bulk:
- REVIVED onto the raw/UNS shape (6): four DriverHostActor primary-gate cases,
the cluster-scoping rebuild, and the real-SDK dual-namespace materialisation
E2E. The behaviour never went dark, only the v2 seeding did.
- FOLDED into a live parity test (3): DeploymentArtifactRawUnsParityTests
already compared RawTagPlan record-equal and RawTagPlan carries array /
historize / alarm intent, so widening its corpus by two tags covers what
three empty placeholder suites had been promising. Those suites are deleted.
- DELETED (4): subjects genuinely retired -- equipment-namespace EquipmentTags,
and both dot-joint {{equip}}.X token suites (that resolver was deleted in
Batch 3; the slash-joint form is covered by DeploymentArtifactEquipRefParityTests).
Falsified, not assumed: removing the seeded UnsTagReference turns two revived
primary-gate tests red; re-homing the SITE-A node to MAIN turns the scoping test
red. Widening a parity corpus also needed direct value assertions -- parity alone
cannot distinguish "both seams right" from "both seams blind".
Runtime.Tests skips 13->3, OpcUaServer.Tests 4->1; all remaining are deliberate
EquipmentDeviceBindingRetired tombstones.
G-7 (deploy-time TagConfig gate). MQTT shipped an Inspect() nobody ever called;
now wired. Replaced the hand-maintained list with a reflection guard, because the
existing test enumerates driver types by hand and so guards renames but can never
notice a gap. NOTE: the first version of that guard was hollow -- it discovered
candidates via GetReferencedAssemblies(), which is circular, since the compiler
elides a reference nothing in the IL uses. Removing MQTT from the map also removed
its Contracts assembly from the manifest, and the guard passed green with the bug
reintroduced. Rewritten to scan the output directory; re-falsified and it now
fails naming MqttTagDefinitionFactory. Residual recorded at the code: Sql,
MTConnect, Calculation and OpcUaClient have no Contracts-side Inspect at all, so
the deploy gate stays weaker than the AdminUI's authoring gate for them.
OPCUACLIENT INTEGRATION TESTS (3 of 4 failing, on master too). Not fixture rot:
the simulator was healthy and Client.CLI read the very same node Good. v3 made the
driver resolve every reference through its authored RawTags table, so a bare
"ns=3;s=StepUp" fails LOCALLY at the resolver with BadNodeIdInvalid and never
reaches the wire -- the tests were exercising the unresolved-reference guard.
Fixed by seeding OpcPlcProfile.RawTags in the deploy artifact's TagConfig shape
and addressing by RawPath. 4/4 pass.
DOCS. Applied the Phase7* -> AddressSpace* rename across the four live docs
(historical bannered files deliberately untouched), resolving the three that are
not renames to their real members. Found en route that the earlier banner pass
missed three files: VirtualTags.md, OpcUaServer.md and ScriptedAlarms.md all
still presented the test-scaffolding GenericDriverNodeManager as the production
dispatch path. All three now carry the correction.
Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
362 lines
20 KiB
C#
362 lines
20 KiB
C#
using System.Collections.Concurrent;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Opc.Ua.Server;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
|
|
|
/// <summary>
|
|
/// #85 — verifies <see cref="AddressSpaceApplier.MaterialiseHierarchy"/> builds the UNS
|
|
/// Area/Line/Equipment folder tree through <see cref="IOpcUaAddressSpaceSink.EnsureFolder"/>.
|
|
/// One pure unit test (recording sink) confirms ordering + parenting; one boot-verify test
|
|
/// drives a real <see cref="OtOpcUaNodeManager"/> and inspects the resulting predefined-node
|
|
/// count to prove the folders land in the SDK address space.
|
|
/// </summary>
|
|
public sealed class AddressSpaceApplierHierarchyTests : IDisposable
|
|
{
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
private readonly string _pkiRoot = Path.Combine(
|
|
Path.GetTempPath(),
|
|
$"otopcua-pki-{Guid.NewGuid():N}");
|
|
|
|
/// <summary>Verifies that MaterialiseHierarchy creates areas, lines, and equipment with correct parent relationships.</summary>
|
|
[Fact]
|
|
public void MaterialiseHierarchy_creates_areas_then_lines_then_equipment_with_correct_parents()
|
|
{
|
|
var sink = new RecordingFolderSink();
|
|
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
|
|
|
var composition = new AddressSpaceComposition(
|
|
UnsAreas: new[] { new UnsAreaProjection("area-1", "Plant North") },
|
|
UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell A") },
|
|
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") },
|
|
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
|
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>());
|
|
|
|
applier.MaterialiseHierarchy(composition);
|
|
|
|
var calls = sink.Calls;
|
|
calls.Count.ShouldBe(3);
|
|
calls[0].ShouldBe(("area-1", null, "Plant North"));
|
|
calls[1].ShouldBe(("line-1", "area-1", "Cell A"));
|
|
calls[2].ShouldBe(("eq-1", "line-1", "Pump-1"));
|
|
}
|
|
|
|
/// <summary>Verifies that orphan equipment without a parent line appears under root.</summary>
|
|
[Fact]
|
|
public void MaterialiseHierarchy_orphan_equipment_hangs_under_root()
|
|
{
|
|
var sink = new RecordingFolderSink();
|
|
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
|
|
|
var composition = new AddressSpaceComposition(
|
|
UnsAreas: Array.Empty<UnsAreaProjection>(),
|
|
UnsLines: Array.Empty<UnsLineProjection>(),
|
|
EquipmentNodes: new[] { new EquipmentNode("eq-orphan", "Orphan", UnsLineId: "") },
|
|
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
|
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>());
|
|
|
|
applier.MaterialiseHierarchy(composition);
|
|
|
|
sink.Calls.Single().ShouldBe(("eq-orphan", null, "Orphan"));
|
|
}
|
|
|
|
/// <summary>Verifies that MaterialiseHierarchy creates folder nodes in a real SDK node manager.</summary>
|
|
[Fact]
|
|
public async Task MaterialiseHierarchy_against_real_SDK_node_manager_creates_folder_nodes()
|
|
{
|
|
await using var host = new OpcUaApplicationHost(
|
|
new OpcUaApplicationHostOptions
|
|
{
|
|
ApplicationName = "OtOpcUa.Hierarchy",
|
|
ApplicationUri = $"urn:OtOpcUa.Hierarchy:{Guid.NewGuid():N}",
|
|
OpcUaPort = AllocateFreePort(),
|
|
PublicHostname = "localhost",
|
|
PkiStoreRoot = _pkiRoot,
|
|
},
|
|
NullLogger<OpcUaApplicationHost>.Instance);
|
|
|
|
var sdkServer = new OtOpcUaSdkServer();
|
|
await host.StartAsync(sdkServer, Ct);
|
|
sdkServer.NodeManager.ShouldNotBeNull();
|
|
|
|
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
|
|
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
|
|
|
applier.MaterialiseHierarchy(new AddressSpaceComposition(
|
|
UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") },
|
|
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
|
|
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
|
|
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
|
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>()));
|
|
|
|
sdkServer.NodeManager!.FolderCount.ShouldBe(5); // 2 areas + 1 line + 2 equipment
|
|
|
|
// Idempotent: re-applying with the same composition doesn't create duplicates.
|
|
applier.MaterialiseHierarchy(new AddressSpaceComposition(
|
|
UnsAreas: new[] { new UnsAreaProjection("area-A", "Area A"), new UnsAreaProjection("area-B", "Area B") },
|
|
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
|
|
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
|
|
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
|
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>()));
|
|
|
|
sdkServer.NodeManager!.FolderCount.ShouldBe(5);
|
|
}
|
|
|
|
/// <summary>Verifies MaterialiseEquipmentTags is idempotent against a real SDK node manager:
|
|
/// applying the same composition twice yields a single Variable node (no duplicates). This is the
|
|
/// restart-safety guarantee — HandleRebuild runs on both the apply path and the
|
|
/// DriverHostActor.RestoreApplied bootstrap path (same RebuildAddressSpace message), so a node
|
|
/// restart re-runs this pass and must not double-materialise.</summary>
|
|
[Fact]
|
|
public async Task MaterialiseEquipmentTags_against_real_SDK_node_manager_is_idempotent()
|
|
{
|
|
await using var host = new OpcUaApplicationHost(
|
|
new OpcUaApplicationHostOptions
|
|
{
|
|
ApplicationName = "OtOpcUa.EquipmentTags",
|
|
ApplicationUri = $"urn:OtOpcUa.EquipmentTags:{Guid.NewGuid():N}",
|
|
OpcUaPort = AllocateFreePort(),
|
|
PublicHostname = "localhost",
|
|
PkiStoreRoot = _pkiRoot,
|
|
},
|
|
NullLogger<OpcUaApplicationHost>.Instance);
|
|
|
|
var sdkServer = new OtOpcUaSdkServer();
|
|
await host.StartAsync(sdkServer, Ct);
|
|
sdkServer.NodeManager.ShouldNotBeNull();
|
|
|
|
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
|
|
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
|
|
|
var composition = new AddressSpaceComposition(
|
|
UnsAreas: Array.Empty<UnsAreaProjection>(),
|
|
UnsLines: Array.Empty<UnsLineProjection>(),
|
|
EquipmentNodes: new[] { new EquipmentNode("eq-1", "filling-eq", UnsLineId: "") },
|
|
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
|
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>())
|
|
{
|
|
EquipmentTags = new[]
|
|
{
|
|
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null),
|
|
},
|
|
};
|
|
|
|
// Equipment folder first (the variable's parent), then the tag pass — applied twice.
|
|
applier.MaterialiseHierarchy(composition);
|
|
applier.MaterialiseEquipmentTags(composition);
|
|
applier.MaterialiseEquipmentTags(composition);
|
|
|
|
sdkServer.NodeManager!.VariableCount.ShouldBe(1); // single variable despite the double-apply
|
|
}
|
|
|
|
/// <summary>
|
|
/// Full v3 structure-materialisation pipeline against a real SDK node manager: real Config entities
|
|
/// (RawFolder → Driver → Device → Tag, plus Area / Line / Equipment and a <c>UnsTagReference</c>) →
|
|
/// <see cref="AddressSpaceComposer.Compose"/> → MaterialiseHierarchy + MaterialiseRawSubtree +
|
|
/// MaterialiseUnsReferences → <see cref="OtOpcUaNodeManager"/>. Proves the <b>dual-namespace</b>
|
|
/// shape lands in a live OPC UA address space: the raw device tree AND the UNS folder tree, with the
|
|
/// raw tag's variable projected into its referencing equipment as a second variable.
|
|
/// <para>Revived from the <c>EquipmentTagsDarkBatch4</c> skip. It was parked pending "the Batch-4
|
|
/// UnsTagReference fan-out", which shipped as v3.0 — but the equipment-tag path it was written
|
|
/// against was RETIRED rather than lit, so unskipping alone would have failed. Rewritten to the
|
|
/// shape that actually replaced it. The sibling <c>MaterialiseHierarchy_against_real_SDK</c> still
|
|
/// covers folders only; this is the layer's only real-SDK proof that variables materialise in both
|
|
/// namespaces.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Raw_and_uns_variables_materialise_end_to_end_against_real_SDK()
|
|
{
|
|
await using var host = new OpcUaApplicationHost(
|
|
new OpcUaApplicationHostOptions
|
|
{
|
|
ApplicationName = "OtOpcUa.EquipmentE2e",
|
|
ApplicationUri = $"urn:OtOpcUa.EquipmentE2e:{Guid.NewGuid():N}",
|
|
OpcUaPort = AllocateFreePort(),
|
|
PublicHostname = "localhost",
|
|
PkiStoreRoot = _pkiRoot,
|
|
},
|
|
NullLogger<OpcUaApplicationHost>.Instance);
|
|
|
|
var sdkServer = new OtOpcUaSdkServer();
|
|
await host.StartAsync(sdkServer, Ct);
|
|
sdkServer.NodeManager.ShouldNotBeNull();
|
|
|
|
// The v3 raw chain: RawFolder "Plant" → Driver "Modbus" → Device "dev1" → Tag "Speed"
|
|
// (RawPath Plant/Modbus/dev1/Speed), plus the UNS tree it is projected into.
|
|
var folder = new RawFolder { RawFolderId = "rf-plant", ClusterId = "c1", Name = "Plant", ParentRawFolderId = null };
|
|
var driver = new DriverInstance { DriverInstanceId = "drv-modbus", ClusterId = "c1", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}", RawFolderId = "rf-plant" };
|
|
var device = new Device { DeviceId = "dev-1", DriverInstanceId = "drv-modbus", Name = "dev1", DeviceConfig = "{}" };
|
|
var speed = new Tag
|
|
{
|
|
TagId = "tag-speed", DeviceId = "dev-1", TagGroupId = null, Name = "Speed", DataType = "Float",
|
|
AccessLevel = TagAccessLevel.Read, TagConfig = "{}",
|
|
};
|
|
var area = new UnsArea { UnsAreaId = "nw-area-filling", ClusterId = "c1", Name = "filling" };
|
|
var line = new UnsLine { UnsLineId = "nw-line-1", UnsAreaId = "nw-area-filling", Name = "line-1" };
|
|
var equipment = new Equipment { EquipmentId = "eq-1", UnsLineId = "nw-line-1", Name = "station-1", MachineCode = "STATION_001" };
|
|
var reference = new UnsTagReference { UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-speed", DisplayNameOverride = null };
|
|
|
|
var composition = AddressSpaceComposer.Compose(
|
|
new[] { area }, new[] { line }, new[] { equipment }, new[] { driver },
|
|
Array.Empty<ScriptedAlarm>(),
|
|
unsTagReferences: new[] { reference }, tags: new[] { speed },
|
|
rawFolders: new[] { folder }, devices: new[] { device });
|
|
|
|
// Compose side: one raw tag at its RawPath, projected once into the referencing equipment.
|
|
var rawTag = composition.RawTags.ShouldHaveSingleItem();
|
|
rawTag.NodeId.ShouldBe("Plant/Modbus/dev1/Speed");
|
|
rawTag.Realm.ShouldBe(AddressSpaceRealm.Raw);
|
|
var unsVar = composition.UnsReferenceVariables.ShouldHaveSingleItem();
|
|
unsVar.NodeId.ShouldBe("filling/line-1/station-1/Speed");
|
|
unsVar.BackingRawPath.ShouldBe("Plant/Modbus/dev1/Speed");
|
|
|
|
var sink = new SdkAddressSpaceSink(sdkServer.NodeManager!);
|
|
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
|
applier.MaterialiseHierarchy(composition);
|
|
applier.MaterialiseRawSubtree(composition);
|
|
applier.MaterialiseUnsReferences(composition);
|
|
|
|
// UNS: filling + line-1 + station-1. Raw: the Plant folder + the Modbus driver + the dev1 device.
|
|
sdkServer.NodeManager!.FolderCount.ShouldBe(6);
|
|
// ONE source value, TWO nodes — the raw tag's variable and its UNS projection. This is the
|
|
// single-source-fan-out shape; a regression that dropped either namespace shows up here as 1.
|
|
sdkServer.NodeManager!.VariableCount.ShouldBe(2);
|
|
}
|
|
|
|
/// <summary>OpcUaServer-001 — a UNS Area / Line rename-only deploy refreshes the EXISTING folder's
|
|
/// DisplayName IN PLACE against a real SDK node manager (no rebuild, no node count change). Proves the
|
|
/// full chain: planner emits a RenamedFolders delta → applier drives the surgical
|
|
/// SdkAddressSpaceSink.UpdateFolderDisplayName → OtOpcUaNodeManager mutates the live FolderState +
|
|
/// ClearChangeMasks. Before the fix, EnsureFolder early-returned on the existing folder and the
|
|
/// DisplayName stayed stale until a full RebuildAddressSpace.</summary>
|
|
[Fact]
|
|
public async Task Area_and_line_rename_updates_existing_folder_display_name_in_place_against_real_SDK()
|
|
{
|
|
await using var host = new OpcUaApplicationHost(
|
|
new OpcUaApplicationHostOptions
|
|
{
|
|
ApplicationName = "OtOpcUa.FolderRename",
|
|
ApplicationUri = $"urn:OtOpcUa.FolderRename:{Guid.NewGuid():N}",
|
|
OpcUaPort = AllocateFreePort(),
|
|
PublicHostname = "localhost",
|
|
PkiStoreRoot = _pkiRoot,
|
|
},
|
|
NullLogger<OpcUaApplicationHost>.Instance);
|
|
|
|
var sdkServer = new OtOpcUaSdkServer();
|
|
await host.StartAsync(sdkServer, Ct);
|
|
sdkServer.NodeManager.ShouldNotBeNull();
|
|
var nm = sdkServer.NodeManager!;
|
|
|
|
var sink = new SdkAddressSpaceSink(nm);
|
|
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
|
|
|
// Materialise the initial hierarchy (area + line + equipment) with the OLD display names.
|
|
var initial = new AddressSpaceComposition(
|
|
UnsAreas: new[] { new UnsAreaProjection("area-1", "Plant North") },
|
|
UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell A") },
|
|
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") },
|
|
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
|
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>());
|
|
applier.MaterialiseHierarchy(initial);
|
|
|
|
nm.FolderCount.ShouldBe(3);
|
|
nm.TryGetFolder("area-1")!.DisplayName.Text.ShouldBe("Plant North");
|
|
nm.TryGetFolder("line-1")!.DisplayName.Text.ShouldBe("Cell A");
|
|
|
|
// Now a rename-only deploy: same ids, new display names on BOTH the area and the line.
|
|
var renamed = new AddressSpaceComposition(
|
|
UnsAreas: new[] { new UnsAreaProjection("area-1", "Plant South") },
|
|
UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell B") },
|
|
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") },
|
|
DriverInstancePlans: Array.Empty<DriverInstancePlan>(),
|
|
ScriptedAlarmPlans: Array.Empty<ScriptedAlarmPlan>());
|
|
|
|
var plan = AddressSpacePlanner.Compute(initial, renamed);
|
|
plan.IsEmpty.ShouldBeFalse(); // rename is no longer a silent no-op
|
|
plan.RenamedFolders.Count.ShouldBe(2);
|
|
|
|
var outcome = applier.Apply(plan);
|
|
|
|
// In-place: no rebuild, no node-count change — only the DisplayNames were swapped.
|
|
outcome.RebuildCalled.ShouldBeFalse();
|
|
nm.FolderCount.ShouldBe(3);
|
|
nm.TryGetFolder("area-1")!.DisplayName.Text.ShouldBe("Plant South");
|
|
nm.TryGetFolder("line-1")!.DisplayName.Text.ShouldBe("Cell B");
|
|
// The unrelated equipment folder is untouched.
|
|
nm.TryGetFolder("eq-1")!.DisplayName.Text.ShouldBe("Pump-1");
|
|
}
|
|
|
|
private static int AllocateFreePort()
|
|
{
|
|
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
|
|
listener.Stop();
|
|
return port;
|
|
}
|
|
|
|
/// <summary>Disposes of resources allocated by this test class.</summary>
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_pkiRoot))
|
|
{
|
|
try { Directory.Delete(_pkiRoot, recursive: true); }
|
|
catch { /* best-effort */ }
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingFolderSink : IOpcUaAddressSpaceSink
|
|
{
|
|
private readonly ConcurrentQueue<(string NodeId, string? Parent, string DisplayName)> _calls = new();
|
|
/// <summary>Gets the list of EnsureFolder calls recorded by this sink.</summary>
|
|
public List<(string NodeId, string? Parent, string DisplayName)> Calls => _calls.ToList();
|
|
|
|
/// <summary>Records a value write (stub implementation for testing).</summary>
|
|
/// <param name="nodeId">The node ID of the variable.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="quality">The OPC UA quality value.</param>
|
|
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
|
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
|
/// <summary>Records an alarm condition write (stub implementation for testing).</summary>
|
|
/// <param name="alarmNodeId">The node ID of the alarm condition.</param>
|
|
/// <param name="state">The full condition state snapshot.</param>
|
|
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
|
public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
|
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
|
/// <summary>Materialises an alarm condition (stub implementation for testing).</summary>
|
|
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId).</param>
|
|
/// <param name="equipmentNodeId">The equipment folder node ID.</param>
|
|
/// <param name="displayName">The condition display name.</param>
|
|
/// <param name="alarmType">The domain alarm type.</param>
|
|
/// <param name="severity">The domain severity.</param>
|
|
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
|
/// <summary>Records a folder creation request.</summary>
|
|
/// <param name="folderNodeId">The node ID of the folder.</param>
|
|
/// <param name="parentNodeId">The node ID of the parent folder, or null for root.</param>
|
|
/// <param name="displayName">The display name of the folder.</param>
|
|
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
|
=> _calls.Enqueue((folderNodeId, parentNodeId, displayName));
|
|
/// <summary>Ensures a variable exists (stub implementation for testing).</summary>
|
|
/// <param name="variableNodeId">The node ID of the variable.</param>
|
|
/// <param name="parentFolderNodeId">The node ID of the parent folder, or null for root.</param>
|
|
/// <param name="displayName">The display name of the variable.</param>
|
|
/// <param name="dataType">The OPC UA built-in type name.</param>
|
|
/// <param name="writable">Whether the node is created read/write.</param>
|
|
/// <param name="historianTagname">The resolved historian tagname (null ⇒ not historized).</param>
|
|
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) { }
|
|
/// <summary>Rebuilds the address space (stub implementation for testing).</summary>
|
|
public void RebuildAddressSpace() { }
|
|
/// <summary>Announces a NodeAdded model-change (stub implementation for testing).</summary>
|
|
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
|
|
public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) { }
|
|
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
|
}
|
|
}
|