using Microsoft.Extensions.Logging.Abstractions; 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; using ZB.MOM.WW.OtOpcUa.OpcUaServer; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// /// v3 Batch 4 (B4-WP3) — the applier's dual-realm materialise passes. /// lights up the ns=Raw device tree /// (containers as folders + tags as variables, each keyed by its RawPath, all in /// ); lights /// up the ns=UNS reference variables (each under its equipment folder in /// ) and wires an Organizes reference UNS→Raw. A historized raw /// tag's UNS reference inherits the SAME historian tagname (both NodeIds → one tagname). Every call carries /// its realm EXPLICITLY. /// public sealed class AddressSpaceApplierRawUnsTests { private static AddressSpaceApplier NewApplier(RealmRecordingSink sink) => new(sink, NullLogger.Instance); [Fact] public void MaterialiseRawSubtree_creates_raw_containers_and_tags_in_raw_realm() { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawContainers = new[] { new RawContainerNode("Plant", null, "Plant", RawNodeKind.Folder), new RawContainerNode("Plant/Modbus", "Plant", "Modbus", RawNodeKind.Driver), new RawContainerNode("Plant/Modbus/dev1", "Plant/Modbus", "dev1", RawNodeKind.Device), }, RawTags = new[] { new RawTagPlan("t1", "Plant/Modbus/dev1/speed", "Plant/Modbus/dev1", "drv-1", "speed", "Float", Writable: true, Alarm: null, ReferencingEquipmentPaths: Array.Empty()), }, }; NewApplier(sink).MaterialiseRawSubtree(composition); // Containers are folders in the Raw realm, parents-before-children. sink.Folders.Select(f => (f.NodeId, f.Realm)).ShouldBe(new[] { ("Plant", AddressSpaceRealm.Raw), ("Plant/Modbus", AddressSpaceRealm.Raw), ("Plant/Modbus/dev1", AddressSpaceRealm.Raw), }); // The tag is a variable keyed by its RawPath, in the Raw realm, parented under its group/device. var v = sink.Variables.ShouldHaveSingleItem(); v.NodeId.ShouldBe("Plant/Modbus/dev1/speed"); v.Parent.ShouldBe("Plant/Modbus/dev1"); v.Realm.ShouldBe(AddressSpaceRealm.Raw); v.Writable.ShouldBeTrue(); v.HistorianTagname.ShouldBeNull(); } [Fact] public void MaterialiseRawSubtree_resolves_historian_tagname_default_and_override() { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawTags = new[] { // Historized, no override → historian tagname defaults to the RawPath. new RawTagPlan("t1", "Plant/A/dev/def", "Plant/A/dev", "drv", "def", "Float", Writable: false, Alarm: null, ReferencingEquipmentPaths: Array.Empty(), IsHistorized: true), // Historized with override → the override wins. new RawTagPlan("t2", "Plant/A/dev/ovr", "Plant/A/dev", "drv", "ovr", "Float", Writable: false, Alarm: null, ReferencingEquipmentPaths: Array.Empty(), IsHistorized: true, HistorianTagname: "WW.Override"), }, }; NewApplier(sink).MaterialiseRawSubtree(composition); var byId = sink.Variables.ToDictionary(v => v.NodeId); byId["Plant/A/dev/def"].HistorianTagname.ShouldBe("Plant/A/dev/def"); // default = RawPath byId["Plant/A/dev/ovr"].HistorianTagname.ShouldBe("WW.Override"); } [Fact] public void MaterialiseRawSubtree_alarm_tag_materialises_a_raw_condition_not_a_variable() { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawTags = new[] { new RawTagPlan("t1", "Plant/A/dev/OverTemp", "Plant/A/dev", "drv", "OverTemp", "Boolean", Writable: false, Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700), ReferencingEquipmentPaths: Array.Empty()), }, }; NewApplier(sink).MaterialiseRawSubtree(composition); sink.Variables.ShouldBeEmpty(); // an alarm tag is a condition, not a value variable var c = sink.Conditions.ShouldHaveSingleItem(); c.NodeId.ShouldBe("Plant/A/dev/OverTemp"); // ConditionId == RawPath c.Parent.ShouldBe("Plant/A/dev"); c.Realm.ShouldBe(AddressSpaceRealm.Raw); c.IsNative.ShouldBeTrue(); // No referencing equipment ⇒ no multi-notifier wiring. sink.NotifierWirings.ShouldBeEmpty(); } [Fact] public void MaterialiseRawSubtree_alarm_tag_wires_a_notifier_per_referencing_equipment_folder() { var sink = new RealmRecordingSink(); // UNS topology so the alarm's referencing-equipment PATHS (Area/Line/Equipment) resolve to the // EquipmentId the equipment folders were materialised under. Two equipment reference the one alarm tag. var composition = new AddressSpaceComposition( new[] { new UnsAreaProjection("a1", "filling") }, new[] { new UnsLineProjection("l1", "a1", "line1"), new UnsLineProjection("l2", "a1", "line2") }, new[] { new EquipmentNode("EQ-1", "station1", "l1"), new EquipmentNode("EQ-2", "station2", "l2"), }, Array.Empty(), Array.Empty()) { RawTags = new[] { new RawTagPlan("t1", "Plant/A/dev/OverTemp", "Plant/A/dev", "drv", "OverTemp", "Boolean", Writable: false, Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700), // The composer emits Area/Line/Equipment NAME paths here. ReferencingEquipmentPaths: new[] { "filling/line1/station1", "filling/line2/station2" }), }, }; NewApplier(sink).MaterialiseRawSubtree(composition); // The single condition is materialised ONCE at the RawPath (Raw realm)... var c = sink.Conditions.ShouldHaveSingleItem(); c.NodeId.ShouldBe("Plant/A/dev/OverTemp"); c.Realm.ShouldBe(AddressSpaceRealm.Raw); // ...and wired as a notifier of BOTH referencing equipment folders — resolved from the name paths to the // logical EquipmentId folder NodeIds, in the Uns realm. ONE wiring call (single condition), not per-root. var w = sink.NotifierWirings.ShouldHaveSingleItem(); w.AlarmNodeId.ShouldBe("Plant/A/dev/OverTemp"); w.AlarmRealm.ShouldBe(AddressSpaceRealm.Raw); w.FolderRealm.ShouldBe(AddressSpaceRealm.Uns); w.FolderNodeIds.ShouldBe(new[] { "EQ-1", "EQ-2" }, ignoreOrder: true); } /// /// Wave C review M2 — composer↔applier PATH-AGREEMENT (the tripwire for the latent DisplayName==Name /// coupling). The composer builds a native alarm's ReferencingEquipmentPaths from the /// Area/Line/Equipment Names; the applier's BuildEquipmentIdByFolderPath inverts those /// paths back to the EquipmentId via the projected DisplayName. Feed a REAL composer output /// (not a hand-built plan) through and assert /// the notifier wired to exactly the backing equipment's id — proving the two path constructions agree /// end-to-end. If a future editable Area/Line display-name feature made DisplayName != Name, this /// inversion (and thus the native-alarm fan-out) would break with no deploy error; this test trips first. /// [Fact] public void Composer_referencing_equipment_paths_resolve_back_to_equipment_ids_in_the_applier() { // Real config: a raw alarm tag (Speed) referenced by one equipment (eq-1 under Area "filling" / // Line "line-1" / Equipment "station-1"). var folder = new RawFolder { RawFolderId = "raw-plant", ClusterId = "c1", Name = "Plant" }; var driver = new DriverInstance { DriverInstanceId = "drv-modbus", ClusterId = "c1", RawFolderId = "raw-plant", Name = "Modbus1", DriverType = "Modbus", DriverConfig = "{}", }; var device = new Device { DeviceId = "dev-1", DriverInstanceId = "drv-modbus", Name = "PLC-A", DeviceConfig = "{}" }; var speed = new Tag { TagId = "t-speed", DeviceId = "dev-1", Name = "Speed", DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{\"alarm\":{\"alarmType\":\"OffNormalAlarm\",\"severity\":700}}", }; var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" }; var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" }; var equip = new Equipment { EquipmentId = "eq-1", UnsLineId = "line-1", Name = "station-1", MachineCode = "STATION_001" }; var refSpeed = new UnsTagReference { UnsTagReferenceId = "ref-speed", EquipmentId = "eq-1", TagId = "t-speed" }; var composition = AddressSpaceComposer.Compose( new[] { area }, new[] { line }, new[] { equip }, new[] { driver }, Array.Empty(), unsTagReferences: new[] { refSpeed }, tags: new[] { speed }, rawFolders: new[] { folder }, devices: new[] { device }, tagGroups: Array.Empty()); // The composer emitted the alarm tag's referencing-equipment path from the Area/Line/Equipment Names. var alarmTag = composition.RawTags.Single(t => t.Alarm is not null); alarmTag.ReferencingEquipmentPaths.ShouldBe(new[] { V3NodeIds.Uns("filling", "line-1", "station-1") }); // The applier inverts that path back to the EquipmentId folder and wires the notifier there. var sink = new RealmRecordingSink(); NewApplier(sink).MaterialiseRawSubtree(composition); var w = sink.NotifierWirings.ShouldHaveSingleItem(); w.AlarmNodeId.ShouldBe(alarmTag.NodeId); w.FolderNodeIds.ShouldBe(new[] { "eq-1" }); // resolved path → EquipmentId } /// Wave C review M2 — when a native alarm HAS referencing equipment paths but NONE resolve to an /// equipment folder (broken ancestry / the latent coupling breaking), the applier surfaces it as a failed /// node (so the degraded-apply meter fires) instead of silently skipping — no notifier is wired, but the /// condition itself is still materialised. [Fact] public void MaterialiseRawSubtree_alarm_with_unresolvable_referencing_equipment_counts_as_failed() { var sink = new RealmRecordingSink(); // No equipment nodes in the composition → the referencing path can't resolve to an EquipmentId folder. var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawTags = new[] { new RawTagPlan("t1", "Plant/A/dev/OverTemp", "Plant/A/dev", "drv", "OverTemp", "Boolean", Writable: false, Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700), ReferencingEquipmentPaths: new[] { "filling/line1/ghost" }), }, }; var failed = NewApplier(sink).MaterialiseRawSubtree(composition); failed.ShouldBeGreaterThanOrEqualTo(1); // resolution failure surfaced (not a silent skip) sink.NotifierWirings.ShouldBeEmpty(); // nothing wired sink.Conditions.ShouldHaveSingleItem(); // the condition itself still materialised } [Fact] public void MaterialiseUnsReferences_creates_uns_variable_and_organizes_edge_inheriting_historian() { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawTags = new[] { // Backing raw tag: historized with an override → the UNS ref must register the SAME tagname. new RawTagPlan("t1", "Plant/A/dev/speed", "Plant/A/dev", "drv", "speed", "Float", Writable: true, Alarm: null, ReferencingEquipmentPaths: new[] { "filling/line1/station1" }, IsHistorized: true, HistorianTagname: "WW.Speed"), }, UnsReferenceVariables = new[] { new UnsReferenceVariable("r1", "EQ-1", "filling/line1/station1/speed", "speed", "Plant/A/dev/speed", "Float", Writable: true, BackingIsHistorized: true, BackingHistorianTagname: "WW.Speed"), }, }; NewApplier(sink).MaterialiseUnsReferences(composition); // The UNS reference is a variable in the Uns realm, parented under its equipment folder (EquipmentId). var v = sink.Variables.ShouldHaveSingleItem(); v.NodeId.ShouldBe("filling/line1/station1/speed"); v.Parent.ShouldBe("EQ-1"); v.Realm.ShouldBe(AddressSpaceRealm.Uns); v.Writable.ShouldBeTrue(); v.HistorianTagname.ShouldBe("WW.Speed"); // inherited from the backing historized raw tag // An Organizes reference UNS→Raw links the two. var r = sink.References.ShouldHaveSingleItem(); r.Source.ShouldBe("filling/line1/station1/speed"); r.SourceRealm.ShouldBe(AddressSpaceRealm.Uns); r.Target.ShouldBe("Plant/A/dev/speed"); r.TargetRealm.ShouldBe(AddressSpaceRealm.Raw); r.ReferenceType.ShouldBe("Organizes"); } /// /// 2026-08-15 review — the inherited historian tagname + array shape come from the /// RECORD, never from a RawTags back-lookup by RawPath. The /// fixture makes the two disagree DELIBERATELY (the raw tag says de-historized + scalar; the reference /// record says historized "WW.Record" + array-of-4) so the assertion can only pass on the record. That /// matters because the record is what the planner diffed to produce the repair delta: materialising /// from anything else would recreate the node with values nobody diffed. /// [Fact] public void MaterialiseUnsReferences_inherits_shape_and_historian_from_the_record_not_the_raw_tag() { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawTags = new[] { new RawTagPlan("t1", "Plant/A/dev/speed", "Plant/A/dev", "drv", "speed", "Float", Writable: true, Alarm: null, ReferencingEquipmentPaths: new[] { "filling/line1/station1" }, IsHistorized: false, HistorianTagname: "WW.RawTag", IsArray: false, ArrayLength: null), }, UnsReferenceVariables = new[] { new UnsReferenceVariable("r1", "EQ-1", "filling/line1/station1/speed", "speed", "Plant/A/dev/speed", "Float", Writable: true, BackingIsHistorized: true, BackingHistorianTagname: "WW.Record", BackingIsArray: true, BackingArrayLength: 4), }, }; NewApplier(sink).MaterialiseUnsReferences(composition); var v = sink.Variables.ShouldHaveSingleItem(); v.HistorianTagname.ShouldBe("WW.Record"); // the record wins; the raw tag says not historized at all v.IsArray.ShouldBeTrue(); v.ArrayLength.ShouldBe(4u); v.Writable.ShouldBeFalse(); // Writable && !IsArray ⇒ an array mirror is read-only } /// Gitea #610: an authored-ReadWrite ARRAY materialises writable on the raw node AND its UNS mirror /// exactly when the driver can write arrays (ArrayWritable / BackingArrayWritable), and /// read-only otherwise; an authored-Read array stays read-only either way. [Theory] [InlineData(true, true, true)] [InlineData(true, false, false)] [InlineData(false, true, false)] public void Array_nodes_are_writable_only_when_the_driver_can_write_arrays(bool authoredWritable, bool arrayWritable, bool expected) { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawTags = new[] { new RawTagPlan("t1", "Plant/A/dev/levels", "Plant/A/dev", "drv", "levels", "Int16", Writable: authoredWritable, Alarm: null, ReferencingEquipmentPaths: Array.Empty(), IsArray: true, ArrayLength: 4, ArrayWritable: arrayWritable), }, UnsReferenceVariables = new[] { new UnsReferenceVariable("r1", "EQ-1", "filling/line1/station1/levels", "levels", "Plant/A/dev/levels", "Int16", Writable: authoredWritable, BackingIsArray: true, BackingArrayLength: 4, BackingArrayWritable: arrayWritable), }, }; var applier = NewApplier(sink); applier.MaterialiseRawSubtree(composition); applier.MaterialiseUnsReferences(composition); sink.Variables.Count.ShouldBe(2); sink.Variables.ShouldAllBe(v => v.Writable == expected && v.IsArray); } /// The historian-resolution semantics carried over verbatim from the old back-lookup: a BLANK /// BackingHistorianTagname on a historized reference falls back to the BackingRawPath (which /// IS the backing raw tag's NodeId, the raw side's own default), and a non-historized reference registers /// no tagname at all even when an override string is present. [Theory] [InlineData(true, null, "Plant/A/dev/speed")] [InlineData(true, " ", "Plant/A/dev/speed")] [InlineData(true, "WW.Override", "WW.Override")] [InlineData(false, "WW.Override", null)] public void MaterialiseUnsReferences_resolves_the_historian_tagname_like_the_raw_side( bool historized, string? tagname, string? expected) { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { UnsReferenceVariables = new[] { new UnsReferenceVariable("r1", "EQ-1", "filling/line1/station1/speed", "speed", "Plant/A/dev/speed", "Float", Writable: true, BackingIsHistorized: historized, BackingHistorianTagname: tagname), }, }; NewApplier(sink).MaterialiseUnsReferences(composition); sink.Variables.ShouldHaveSingleItem().HistorianTagname.ShouldBe(expected); } /// /// 2026-08-15 surgical-deploys (Task 6) — the subset overload materialises ONLY the tags/containers it /// is handed, while still resolving native-alarm notifier targets from the FULL composition's equipment /// map. Both halves matter: narrowing the iteration is the whole point (a one-tag deploy used to re-walk /// every node on the server), and the map has to stay full because the equipment a raw alarm fans to is /// not part of any raw-tag delta — building it from the subset would silently drop the fan-out. /// [Fact] public void MaterialiseRawSubtree_with_a_subset_ensures_only_the_subset_but_resolves_notifiers_from_the_full_composition() { var sink = new RealmRecordingSink(); var untouched = new RawTagPlan("t0", "Plant/A/dev/speed", "Plant/A/dev", "drv", "speed", "Float", Writable: true, Alarm: null, ReferencingEquipmentPaths: Array.Empty()); var added = new RawTagPlan("t1", "Plant/A/dev/OverTemp", "Plant/A/dev", "drv", "OverTemp", "Boolean", Writable: false, Alarm: new EquipmentTagAlarmInfo("OffNormalAlarm", 700), ReferencingEquipmentPaths: new[] { "filling/line1/station1" }); var composition = new AddressSpaceComposition( new[] { new UnsAreaProjection("a1", "filling") }, new[] { new UnsLineProjection("l1", "a1", "line1") }, new[] { new EquipmentNode("EQ-1", "station1", "l1") }, Array.Empty(), Array.Empty()) { RawContainers = new[] { new RawContainerNode("Plant", null, "Plant", RawNodeKind.Folder), new RawContainerNode("Plant/A", "Plant", "A", RawNodeKind.Driver), new RawContainerNode("Plant/A/dev", "Plant/A", "dev", RawNodeKind.Device), }, RawTags = new[] { untouched, added }, }; // The delta of a one-tag add: no new containers, exactly one new tag. var failed = NewApplier(sink).MaterialiseRawSubtree( composition, onlyContainers: Array.Empty(), onlyTags: new[] { added }); failed.ShouldBe(0); sink.Folders.ShouldBeEmpty(); // the three existing containers were NOT re-ensured sink.Variables.ShouldBeEmpty(); // the untouched value tag was NOT re-ensured sink.Conditions.ShouldHaveSingleItem().NodeId.ShouldBe("Plant/A/dev/OverTemp"); // …and the notifier still resolved through the FULL composition's Area/Line/Equipment map. sink.NotifierWirings.ShouldHaveSingleItem().FolderNodeIds.ShouldBe(new[] { "EQ-1" }); } /// A null subset (the rebuild path) still means "the whole composition" — the overload is additive /// and every pre-existing call site keeps its behaviour. [Fact] public void MaterialiseRawSubtree_with_no_subset_still_materialises_the_whole_composition() { var sink = new RealmRecordingSink(); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { RawContainers = new[] { new RawContainerNode("Plant", null, "Plant", RawNodeKind.Folder) }, RawTags = new[] { new RawTagPlan("t0", "Plant/a", "Plant", "drv", "a", "Float", Writable: true, Alarm: null, ReferencingEquipmentPaths: Array.Empty()), new RawTagPlan("t1", "Plant/b", "Plant", "drv", "b", "Float", Writable: true, Alarm: null, ReferencingEquipmentPaths: Array.Empty()), }, }; NewApplier(sink).MaterialiseRawSubtree(composition); sink.Folders.Select(f => f.NodeId).ShouldBe(new[] { "Plant" }); sink.Variables.Select(v => v.NodeId).ShouldBe(new[] { "Plant/a", "Plant/b" }); } /// 2026-08-15 surgical-deploys (Task 6) — the UNS-reference subset overload materialises only the /// references it is handed (with their Organizes edges), leaving every other reference in the composition /// untouched. [Fact] public void MaterialiseUnsReferences_with_a_subset_ensures_only_the_subset() { var sink = new RealmRecordingSink(); var untouched = new UnsReferenceVariable("r0", "EQ-1", "filling/line1/station1/speed", "speed", "Plant/A/dev/speed", "Float", Writable: true); var added = new UnsReferenceVariable("r1", "EQ-1", "filling/line1/station1/temp", "temp", "Plant/A/dev/temp", "Float", Writable: false); var composition = new AddressSpaceComposition( Array.Empty(), Array.Empty(), Array.Empty()) { UnsReferenceVariables = new[] { untouched, added }, }; NewApplier(sink).MaterialiseUnsReferences(composition, only: new[] { added }); sink.Variables.ShouldHaveSingleItem().NodeId.ShouldBe("filling/line1/station1/temp"); sink.References.ShouldHaveSingleItem().Target.ShouldBe("Plant/A/dev/temp"); } /// A capturing sink that records (nodeId, realm) for every materialise + the Organizes edges. private sealed class RealmRecordingSink : IOpcUaAddressSpaceSink { public List<(string NodeId, string? Parent, AddressSpaceRealm Realm)> Folders { get; } = new(); public List<(string NodeId, string? Parent, bool Writable, string? HistorianTagname, AddressSpaceRealm Realm, bool IsArray, uint? ArrayLength)> Variables { get; } = new(); public List<(string NodeId, string? Parent, bool IsNative, AddressSpaceRealm Realm)> Conditions { get; } = new(); public List<(string Source, AddressSpaceRealm SourceRealm, string Target, AddressSpaceRealm TargetRealm, string ReferenceType)> References { get; } = new(); public List<(string AlarmNodeId, AddressSpaceRealm AlarmRealm, IReadOnlyList FolderNodeIds, AddressSpaceRealm FolderRealm)> NotifierWirings { get; } = new(); public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm) => Folders.Add((folderNodeId, parentNodeId, realm)); public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, bool refuseArrayOnScalar = false) => Variables.Add((variableNodeId, parentFolderNodeId, writable, historianTagname, realm, isArray, arrayLength)); public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) => Conditions.Add((alarmNodeId, equipmentNodeId, isNative, realm)); public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) => NotifierWirings.Add((alarmNodeId, alarmRealm, notifierFolderNodeIds, notifierFolderRealm)); public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") => References.Add((sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType)); public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { } public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { } public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { } public void RebuildAddressSpace() { } public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm) { } } }