feat(historian-gateway): DriverDataType->HistorianDataType mapper + write-gap fallbacks (matrix-guarded)
Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the driver-agnostic <see cref="DriverDataType"/> onto the gateway's
|
||||||
|
/// <see cref="HistorianDataType"/> for tag provisioning + historical writes.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 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:
|
||||||
|
/// <see cref="DriverDataType.UInt16"/> maps to <see cref="HistorianDataType.Uint4"/> (the historian's
|
||||||
|
/// <c>UInt2</c> write path is not proven). String / DateTime / Reference are not historized in v1
|
||||||
|
/// and throw <see cref="NotSupportedException"/>; callers that want to skip them without catching an
|
||||||
|
/// exception should consult <see cref="IsHistorizable(DriverDataType)"/> first.
|
||||||
|
/// </remarks>
|
||||||
|
internal static class HistorianTypeMapper
|
||||||
|
{
|
||||||
|
/// <summary>Maps a driver data type to the historian data type used for provisioning/writes.</summary>
|
||||||
|
/// <param name="dataType">The driver-agnostic data type.</param>
|
||||||
|
/// <returns>The matching <see cref="HistorianDataType"/>.</returns>
|
||||||
|
/// <exception cref="NotSupportedException">
|
||||||
|
/// The type is explicitly deferred (string/datetime/reference) or a future, unclassified member.
|
||||||
|
/// </exception>
|
||||||
|
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."),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when <paramref name="dataType"/> is one of the nine historizable numeric types — lets the
|
||||||
|
/// provisioning hook skip deferred types without catching <see cref="NotSupportedException"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataType">The driver-agnostic data type.</param>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
+52
@@ -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<NotSupportedException>(() => 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<DriverDataType>())
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user