From 3226b8781875f2577e20e82206e4a89c0cfc9e28 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 26 Jun 2026 16:32:38 -0400 Subject: [PATCH] feat(historian-gateway): DriverDataType->HistorianDataType mapper + write-gap fallbacks (matrix-guarded) Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii --- .../Mapping/HistorianTypeMapper.cs | 63 +++++++++++++++++++ .../Mapping/HistorianTypeMapperTests.cs | 52 +++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs new file mode 100644 index 00000000..97631961 --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs @@ -0,0 +1,63 @@ +using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; + +/// +/// Maps the driver-agnostic onto the gateway's +/// for tag provisioning + historical writes. +/// +/// +/// Only the nine numeric types are historizable on the gateway's analog write path. Two of them +/// fall back to a wider historian type because the narrower one's write path is deferred upstream: +/// maps to (the historian's +/// UInt2 write path is not proven). String / DateTime / Reference are not historized in v1 +/// and throw ; callers that want to skip them without catching an +/// exception should consult first. +/// +internal static class HistorianTypeMapper +{ + /// Maps a driver data type to the historian data type used for provisioning/writes. + /// The driver-agnostic data type. + /// The matching . + /// + /// The type is explicitly deferred (string/datetime/reference) or a future, unclassified member. + /// + public static HistorianDataType ToHistorianDataType(DriverDataType dataType) => dataType switch + { + DriverDataType.Boolean => HistorianDataType.Int1, + DriverDataType.Int16 => HistorianDataType.Int2, + DriverDataType.Int32 => HistorianDataType.Int4, + DriverDataType.Int64 => HistorianDataType.Int8, + DriverDataType.UInt16 => HistorianDataType.Uint4, // UInt2 write path deferred upstream → widen + DriverDataType.UInt32 => HistorianDataType.Uint4, + DriverDataType.UInt64 => HistorianDataType.Uint8, + DriverDataType.Float32 => HistorianDataType.Float, + DriverDataType.Float64 => HistorianDataType.Double, + DriverDataType.String or DriverDataType.DateTime or DriverDataType.Reference => + throw new NotSupportedException( + $"DriverDataType.{dataType} is not historized in v1 " + + "(string/datetime/reference writes are deferred — gated on the analog SQL write path)."), + _ => throw new NotSupportedException( + $"DriverDataType.{dataType} is not classified for historian write mapping — add a HistorianDataType mapping."), + }; + + /// + /// True when is one of the nine historizable numeric types — lets the + /// provisioning hook skip deferred types without catching . + /// + /// The driver-agnostic data type. + public static bool IsHistorizable(DriverDataType dataType) => dataType switch + { + DriverDataType.Boolean + or DriverDataType.Int16 + or DriverDataType.Int32 + or DriverDataType.Int64 + or DriverDataType.UInt16 + or DriverDataType.UInt32 + or DriverDataType.UInt64 + or DriverDataType.Float32 + or DriverDataType.Float64 => true, + _ => false, + }; +} diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs new file mode 100644 index 00000000..1dde2678 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/HistorianTypeMapperTests.cs @@ -0,0 +1,52 @@ +using Xunit; +using ZB.MOM.WW.OtOpcUa.Core.Abstractions; // DriverDataType +using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; // HistorianDataType +using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping; + +public sealed class HistorianTypeMapperTests +{ + [Theory] + [InlineData(DriverDataType.Boolean, HistorianDataType.Int1)] + [InlineData(DriverDataType.Int16, HistorianDataType.Int2)] + [InlineData(DriverDataType.Int32, HistorianDataType.Int4)] + [InlineData(DriverDataType.Int64, HistorianDataType.Int8)] + [InlineData(DriverDataType.UInt16, HistorianDataType.Uint4)] // fallback: UInt2 write deferred upstream + [InlineData(DriverDataType.UInt32, HistorianDataType.Uint4)] + [InlineData(DriverDataType.UInt64, HistorianDataType.Uint8)] + [InlineData(DriverDataType.Float32, HistorianDataType.Float)] + [InlineData(DriverDataType.Float64, HistorianDataType.Double)] + public void Maps_writable_numeric_types(DriverDataType d, HistorianDataType expected) + => Assert.Equal(expected, HistorianTypeMapper.ToHistorianDataType(d)); + + [Theory] + [InlineData(DriverDataType.String)] + [InlineData(DriverDataType.DateTime)] + [InlineData(DriverDataType.Reference)] + public void Deferred_types_throw_NotSupported_with_clear_message(DriverDataType d) + { + var ex = Assert.Throws(() => HistorianTypeMapper.ToHistorianDataType(d)); + Assert.Contains("not historized in v1", ex.Message); // human-actionable, no tag value leaked + } + + [Fact] // matrix guard: a new DriverDataType member must be classified (mapped or explicitly deferred) + public void Every_DriverDataType_member_is_classified() + { + foreach (var d in Enum.GetValues()) + { + try { _ = HistorianTypeMapper.ToHistorianDataType(d); } + catch (NotSupportedException) { /* explicitly deferred — acceptable */ } + // any OTHER exception (e.g. ArgumentOutOfRangeException from an unhandled new member) fails the test + } + } + + [Theory] + [InlineData(DriverDataType.Boolean, true)] + [InlineData(DriverDataType.Float64, true)] + [InlineData(DriverDataType.String, false)] + [InlineData(DriverDataType.DateTime, false)] + [InlineData(DriverDataType.Reference, false)] + public void IsHistorizable_matches_writable_set(DriverDataType d, bool expected) + => Assert.Equal(expected, HistorianTypeMapper.IsHistorizable(d)); +}