using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa; /// /// Phase 6.4 Stream D: materializes the OPC 40010 Machinery companion-spec Identification /// sub-folder under an Equipment node. Reads the nine decision-#139 columns off the /// row and emits one property per non-null field. /// /// /// Pure-function shape — testable without a real OPC UA node manager. The caller /// passes the builder scoped to the Equipment node; this class handles the Identification /// sub-folder creation + per-field calls. /// /// ACL binding: the sub-folder + its properties inherit the Equipment scope's /// grants (no new scope level). Phase 6.2's trie treats them as part of the Equipment /// ScopeId — a user with Equipment-level grant reads Identification; a user without the /// grant gets BadUserAccessDenied on both the Equipment node + its Identification variables. /// See docs/v2/acl-design.md §Identification cross-reference. /// /// The nine fields per decision #139 are exposed exactly when they carry a non-null /// value. A row with all nine null produces no Identification sub-folder at all — the /// caller can use to skip the Folder call entirely /// and avoid a pointless empty folder appearing in browse trees. /// public static class IdentificationFolderBuilder { /// Browse + display name of the sub-folder — fixed per OPC 40010 convention. public const string FolderName = "Identification"; /// /// Canonical decision #139 field set exposed in the Identification sub-folder. Order /// matches the decision-log entry so any browse-order reader can cross-reference /// without re-sorting. /// public static IReadOnlyList FieldNames { get; } = new[] { "Manufacturer", "Model", "SerialNumber", "HardwareRevision", "SoftwareRevision", "YearOfConstruction", "AssetLocation", "ManufacturerUri", "DeviceManualUri", }; /// True when the equipment row has at least one non-null Identification field. public static bool HasAnyFields(Equipment equipment) { ArgumentNullException.ThrowIfNull(equipment); return equipment.Manufacturer is not null || equipment.Model is not null || equipment.SerialNumber is not null || equipment.HardwareRevision is not null || equipment.SoftwareRevision is not null || equipment.YearOfConstruction is not null || equipment.AssetLocation is not null || equipment.ManufacturerUri is not null || equipment.DeviceManualUri is not null; } /// /// Build the Identification sub-folder under . No-op /// when every field is null. Returns the sub-folder builder (or null when no-op) so /// callers can attach additional nodes underneath if needed. /// public static IAddressSpaceBuilder? Build(IAddressSpaceBuilder equipmentBuilder, Equipment equipment) { ArgumentNullException.ThrowIfNull(equipmentBuilder); ArgumentNullException.ThrowIfNull(equipment); if (!HasAnyFields(equipment)) return null; var folder = equipmentBuilder.Folder(FolderName, FolderName); AddIfPresent(folder, "Manufacturer", DriverDataType.String, equipment.Manufacturer); AddIfPresent(folder, "Model", DriverDataType.String, equipment.Model); AddIfPresent(folder, "SerialNumber", DriverDataType.String, equipment.SerialNumber); AddIfPresent(folder, "HardwareRevision", DriverDataType.String, equipment.HardwareRevision); AddIfPresent(folder, "SoftwareRevision", DriverDataType.String, equipment.SoftwareRevision); AddIfPresent(folder, "YearOfConstruction", DriverDataType.Int32, equipment.YearOfConstruction is null ? null : (object)(int)equipment.YearOfConstruction.Value); AddIfPresent(folder, "AssetLocation", DriverDataType.String, equipment.AssetLocation); AddIfPresent(folder, "ManufacturerUri", DriverDataType.String, equipment.ManufacturerUri); AddIfPresent(folder, "DeviceManualUri", DriverDataType.String, equipment.DeviceManualUri); return folder; } private static void AddIfPresent(IAddressSpaceBuilder folder, string name, DriverDataType dataType, object? value) { if (value is null) return; folder.AddProperty(name, dataType, value); } }