diff --git a/FluentAAS/FluentAAS.Templates.Tests/TechnicalData/TechnicalDataBuilderTests.cs b/FluentAAS/FluentAAS.Templates.Tests/TechnicalData/TechnicalDataBuilderTests.cs new file mode 100644 index 0000000..992a024 --- /dev/null +++ b/FluentAAS/FluentAAS.Templates.Tests/TechnicalData/TechnicalDataBuilderTests.cs @@ -0,0 +1,117 @@ +using AasCore.Aas3_0; +using FluentAAS.Builder; +using FluentAAS.Templates.TechnicalData; +using Shouldly; + +namespace FluentAAS.Templates.Tests.TechnicalData; + +public class TechnicalDataBuilderTests +{ + [Fact] + public void BuildTechnicalData_ShouldFail_WhenMandatoryParametersAreMissing() + { + var envBuilder = AasBuilder.Create(); + var shellBuilder = envBuilder.AddShell("urn:aas:example:shell:technical-data", "DriveShell"); + + var sut = shellBuilder.AddTechnicalData("urn:aas:example:submodel:technical-data") + .WithMotorPerformance(motor => motor + .WithRatedVoltage(400m) + .WithRatedCurrent(82.5m)); + + var ex = Record.Exception(() => sut.BuildTechnicalData()); + + ex.ShouldBeOfType(); + ex.Message.ShouldContain("MotorPerformance.RatedPower"); + ex.Message.ShouldContain("MotorPerformance.RatedSpeed"); + ex.Message.ShouldContain("BearingCharacteristics.InnerDiameter"); + } + + [Fact] + public void BuildTechnicalData_ShouldFail_WhenUnitIsInvalid() + { + var envBuilder = AasBuilder.Create(); + var shellBuilder = envBuilder.AddShell("urn:aas:example:shell:technical-data", "DriveShell"); + + var ex = Record.Exception(() => shellBuilder.AddTechnicalData("urn:aas:example:submodel:technical-data") + .WithMotorPerformance(motor => motor.WithRatedVoltage(400m, "mm"))); + + ex.ShouldBeOfType(); + ex.Message.ShouldContain("Unit 'mm' is invalid for 'RatedVoltage'"); + } + + [Fact] + public void BuildTechnicalData_ShouldFail_WhenSemanticIdDoesNotMatchEclassCatalogEntry() + { + var envBuilder = AasBuilder.Create(); + var shellBuilder = envBuilder.AddShell("urn:aas:example:shell:technical-data", "DriveShell"); + + var ex = Record.Exception(() => shellBuilder.AddTechnicalData("urn:aas:example:submodel:technical-data") + .WithMotorPerformance(motor => motor.WithRatedVoltage(400m, "V", TechnicalDataSemantics.RatedCurrent))); + + ex.ShouldBeOfType(); + ex.Message.ShouldContain("Semantic ID mismatch for 'RatedVoltage'"); + } + + [Fact] + public void BuildTechnicalData_ShouldCreateStructuredGroups_WithEclassSemanticIds() + { + var envBuilder = AasBuilder.Create(); + var shellBuilder = envBuilder.AddShell("urn:aas:example:shell:technical-data", "DriveShell"); + + shellBuilder.AddTechnicalData("urn:aas:example:submodel:technical-data") + .WithMotorPerformance(motor => motor + .WithRatedVoltage(400m, "V", TechnicalDataSemantics.RatedVoltage) + .WithRatedCurrent(82.5m, "A", TechnicalDataSemantics.RatedCurrent) + .WithRatedPower(45m, "kW", TechnicalDataSemantics.RatedPower) + .WithRatedSpeed(1470m, "1/min", TechnicalDataSemantics.RatedSpeed)) + .WithBearingCharacteristics(bearing => bearing + .WithInnerDiameter(50m, "mm", TechnicalDataSemantics.InnerDiameter) + .WithOuterDiameter(90m, "mm", TechnicalDataSemantics.OuterDiameter) + .WithWidth(20m, "mm", TechnicalDataSemantics.Width) + .WithLimitingSpeed(6500m, "1/min", TechnicalDataSemantics.LimitingSpeed)) + .BuildTechnicalData(); + + var env = envBuilder.Build(); + + var submodel = env.Submodels!.Single(sm => sm.Id == "urn:aas:example:submodel:technical-data"); + submodel.SemanticId!.Keys.Single().Value.ShouldBe(TechnicalDataSemantics.SubmodelTechnicalData); + + var motorGroup = submodel.SubmodelElements! + .OfType() + .Single(x => x.IdShort == TechnicalDataIdentifiers.MotorPerformance); + + var ratedVoltage = motorGroup.Value! + .OfType() + .Single(x => x.IdShort == TechnicalDataIdentifiers.RatedVoltage); + + ratedVoltage.Value.ShouldBe("400"); + ratedVoltage.Category.ShouldBe("V"); + ratedVoltage.SemanticId!.Keys.Single().Value.ShouldBe(TechnicalDataSemantics.RatedVoltage); + } + + [Fact] + public void InlineComposition_ShouldBuildEnvironment() + { + var envBuilder = AasBuilder.Create(); + envBuilder.AddShell("urn:aas:example:drivetrain:1000", "DriveTrainAAS") + .AddTechnicalData("urn:aas:submodel:technical-data:drivetrain:1000") + .WithMotorPerformance(motor => motor + .WithRatedVoltage(400m, "V", TechnicalDataSemantics.RatedVoltage) + .WithRatedCurrent(82.5m, "A", TechnicalDataSemantics.RatedCurrent) + .WithRatedPower(45m, "kW", TechnicalDataSemantics.RatedPower) + .WithRatedSpeed(1470m, "1/min", TechnicalDataSemantics.RatedSpeed)) + .WithBearingCharacteristics(bearing => bearing + .WithInnerDiameter(50m, "mm", TechnicalDataSemantics.InnerDiameter) + .WithOuterDiameter(90m, "mm", TechnicalDataSemantics.OuterDiameter) + .WithWidth(20m, "mm", TechnicalDataSemantics.Width) + .WithLimitingSpeed(6500m, "1/min", TechnicalDataSemantics.LimitingSpeed)) + .BuildTechnicalData(); + + var env = envBuilder.Build(); + + env.AssetAdministrationShells.ShouldNotBeNull(); + env.AssetAdministrationShells!.Count.ShouldBe(1); + env.Submodels.ShouldNotBeNull(); + env.Submodels!.Count.ShouldBe(1); + } +} diff --git a/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataBuilder.cs b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataBuilder.cs new file mode 100644 index 0000000..25b6678 --- /dev/null +++ b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataBuilder.cs @@ -0,0 +1,345 @@ +using FluentAAS.Builder; +using FluentAAS.Builder.SubModel; + +namespace FluentAAS.Templates.TechnicalData; + +/// +/// Fluent builder for composing an IDTA Technical Data submodel with typed groups, +/// ECLASS semantic mapping and IEC 61360-oriented validation. +/// The builder fails fast so invalid technical parameter sets are rejected during composition. +/// +public sealed class TechnicalDataBuilder +{ + private readonly IShellBuilder _shellBuilder; + private string _id; + private string _idShort; + + private readonly Dictionary _groups = new(StringComparer.Ordinal); + + internal TechnicalDataBuilder(IShellBuilder shellBuilder, string id, string idShort) + { + _shellBuilder = shellBuilder ?? throw new ArgumentNullException(nameof(shellBuilder)); + + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Submodel id must not be empty.", nameof(id)); + } + + if (string.IsNullOrWhiteSpace(idShort)) + { + throw new ArgumentException("Submodel idShort must not be empty.", nameof(idShort)); + } + + _id = id; + _idShort = idShort; + } + + /// + /// Overrides the submodel identifiers used for the generated Technical Data submodel. + /// This allows callers to align the template with project-specific naming and ID strategies. + /// + /// Globally unique submodel identifier. + /// Human-readable short identifier. + /// The current builder for fluent chaining. + public TechnicalDataBuilder WithIds(string id, string idShort) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Submodel id must not be empty.", nameof(id)); + } + + if (string.IsNullOrWhiteSpace(idShort)) + { + throw new ArgumentException("Submodel idShort must not be empty.", nameof(idShort)); + } + + _id = id; + _idShort = idShort; + return this; + } + + /// + /// Configures the strongly typed motor performance group. + /// Using this method guarantees that only motor-performance properties are assigned in this group. + /// + /// Callback that fills the motor performance properties. + /// The current builder for fluent chaining. + public TechnicalDataBuilder WithMotorPerformance(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + var groupBuilder = new MotorPerformanceGroupBuilder(this); + configure(groupBuilder); + + return this; + } + + /// + /// Configures the strongly typed bearing characteristics group. + /// This prevents accidental mixing of unrelated technical parameters. + /// + /// Callback that fills the bearing characteristic properties. + /// The current builder for fluent chaining. + public TechnicalDataBuilder WithBearingCharacteristics(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + var groupBuilder = new BearingCharacteristicsGroupBuilder(this); + configure(groupBuilder); + + return this; + } + + /// + /// Validates and builds the Technical Data submodel, then attaches it to the parent shell. + /// This central build step ensures required parameters, units and semantics are validated before persistence. + /// + /// The parent shell builder to continue composing the AAS. + /// + /// Thrown when required properties are missing or semantic mappings are invalid. + /// + public IShellBuilder BuildTechnicalData() + { + ValidateRequiredParameters(); + + var submodelBuilder = new SubmodelBuilder(_shellBuilder, _id, _idShort) + .WithSemanticId(ReferenceFactory.GlobalConceptDescription(TechnicalDataSemantics.SubmodelTechnicalData)); + + foreach (var group in _groups.Values.OrderBy(x => x.IdShort, StringComparer.Ordinal)) + { + var collection = new SubmodelElementCollection + { + IdShort = group.IdShort, + Value = [], + SemanticId = ReferenceFactory.GlobalSemanticReference($"{TechnicalDataSemantics.SubmodelTechnicalData}/{group.IdShort}") + }; + + foreach (var assignment in group.Assignments.Values.OrderBy(x => x.Definition.IdShort, StringComparer.Ordinal)) + { + ValidateEclassCatalogMapping(assignment.Definition.EclassIrdi); + + var property = new Property(assignment.Definition.ValueType) + { + IdShort = assignment.Definition.IdShort, + Value = assignment.Value, + ValueId = ReferenceFactory.GlobalSemanticReference(assignment.Definition.EclassIrdi), + SemanticId = ReferenceFactory.GlobalConceptDescription(assignment.Definition.EclassIrdi), + Category = assignment.Unit + }; + + collection.Value!.Add(property); + } + + submodelBuilder.AddElement(collection); + } + + var submodel = submodelBuilder.BuildSubmodel(); + _shellBuilder.AddSubmodelReference(submodel); + + return _shellBuilder; + } + + private void ValidateRequiredParameters() + { + var missing = new List(); + + foreach (var groupIdShort in new[] { TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.BearingCharacteristics }) + { + if (!_groups.TryGetValue(groupIdShort, out var group)) + { + missing.AddRange(TechnicalDataEclassCatalog.RequiredInGroup(groupIdShort).Select(x => $"{groupIdShort}.{x.IdShort}")); + continue; + } + + var required = TechnicalDataEclassCatalog.RequiredInGroup(groupIdShort); + missing.AddRange(required.Where(x => !group.Assignments.ContainsKey(x.IdShort)).Select(x => $"{groupIdShort}.{x.IdShort}")); + } + + if (missing.Count == 0) + { + return; + } + + throw new InvalidOperationException("Technical data is incomplete. Missing mandatory parameters: " + string.Join(", ", missing)); + } + + private static void ValidateEclassCatalogMapping(string eclassIrdi) + { + if (!TechnicalDataEclassCatalog.IsKnownEclassReference(eclassIrdi)) + { + throw new InvalidOperationException($"The ECLASS semantic ID '{eclassIrdi}' is not available in the integrated ECLASS catalog map."); + } + } + + internal void UpsertDecimalProperty(string groupIdShort, string idShort, decimal value, string unit, string? semanticIdOverride) + { + var definition = TechnicalDataEclassCatalog.ForIdShort(idShort); + + if (!string.Equals(definition.GroupIdShort, groupIdShort, StringComparison.Ordinal)) + { + throw new InvalidOperationException($"Property '{idShort}' is not allowed in group '{groupIdShort}'."); + } + + if (definition.ValueType != DataTypeDefXsd.Decimal) + { + throw new InvalidOperationException($"Property '{idShort}' does not accept decimal values according to IEC 61360 type constraints."); + } + + if (!definition.AllowedUnits.Contains(unit, StringComparer.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"Unit '{unit}' is invalid for '{idShort}'. Allowed IEC 61360 units: {string.Join(", ", definition.AllowedUnits)}."); + } + + if (!string.IsNullOrWhiteSpace(semanticIdOverride) && !string.Equals(semanticIdOverride, definition.EclassIrdi, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"Semantic ID mismatch for '{idShort}'. Expected '{definition.EclassIrdi}' but got '{semanticIdOverride}'."); + } + + var group = GetOrCreateGroup(groupIdShort); + group.Assignments[idShort] = new PropertyAssignment(definition, value.ToString(System.Globalization.CultureInfo.InvariantCulture), unit); + } + + private GroupAssignment GetOrCreateGroup(string groupIdShort) + { + if (_groups.TryGetValue(groupIdShort, out var existing)) + { + return existing; + } + + var created = new GroupAssignment(groupIdShort); + _groups[groupIdShort] = created; + return created; + } + + private sealed class GroupAssignment(string idShort) + { + public string IdShort { get; } = idShort; + + public Dictionary Assignments { get; } = new(StringComparer.Ordinal); + } + + private sealed record PropertyAssignment( + TechnicalDataEclassCatalog.PropertyDefinition Definition, + string Value, + string Unit); + + /// + /// Group builder for motor performance properties. + /// The API is deliberately constrained to valid motor fields for safer, more predictable template usage. + /// + public sealed class MotorPerformanceGroupBuilder(TechnicalDataBuilder parent) + { + /// + /// Sets rated voltage with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric rated voltage value. + /// Unit symbol (default: V). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current motor performance builder. + public MotorPerformanceGroupBuilder WithRatedVoltage(decimal value, string unit = "V", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedVoltage, value, unit, semanticId); + return this; + } + + /// + /// Sets rated current with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric rated current value. + /// Unit symbol (default: A). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current motor performance builder. + public MotorPerformanceGroupBuilder WithRatedCurrent(decimal value, string unit = "A", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedCurrent, value, unit, semanticId); + return this; + } + + /// + /// Sets rated power with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric rated power value. + /// Unit symbol (default: kW). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current motor performance builder. + public MotorPerformanceGroupBuilder WithRatedPower(decimal value, string unit = "kW", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedPower, value, unit, semanticId); + return this; + } + + /// + /// Sets rated speed with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric rated speed value. + /// Unit symbol (default: 1/min). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current motor performance builder. + public MotorPerformanceGroupBuilder WithRatedSpeed(decimal value, string unit = "1/min", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedSpeed, value, unit, semanticId); + return this; + } + } + + /// + /// Group builder for bearing characteristics. + /// The focused API helps callers provide structurally valid bearing data with consistent semantics. + /// + public sealed class BearingCharacteristicsGroupBuilder(TechnicalDataBuilder parent) + { + /// + /// Sets bearing inner diameter with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric inner diameter value. + /// Unit symbol (default: mm). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current bearing characteristics builder. + public BearingCharacteristicsGroupBuilder WithInnerDiameter(decimal value, string unit = "mm", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.InnerDiameter, value, unit, semanticId); + return this; + } + + /// + /// Sets bearing outer diameter with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric outer diameter value. + /// Unit symbol (default: mm). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current bearing characteristics builder. + public BearingCharacteristicsGroupBuilder WithOuterDiameter(decimal value, string unit = "mm", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.OuterDiameter, value, unit, semanticId); + return this; + } + + /// + /// Sets bearing width with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric width value. + /// Unit symbol (default: mm). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current bearing characteristics builder. + public BearingCharacteristicsGroupBuilder WithWidth(decimal value, string unit = "mm", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.Width, value, unit, semanticId); + return this; + } + + /// + /// Sets bearing limiting speed with IEC 61360-conform unit and optional explicit semantic check. + /// + /// Numeric limiting speed value. + /// Unit symbol (default: 1/min). + /// Optional ECLASS IRDI for explicit semantic conformity check. + /// The current bearing characteristics builder. + public BearingCharacteristicsGroupBuilder WithLimitingSpeed(decimal value, string unit = "1/min", string? semanticId = null) + { + parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.LimitingSpeed, value, unit, semanticId); + return this; + } + } +} diff --git a/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataEclassCatalog.cs b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataEclassCatalog.cs new file mode 100644 index 0000000..689d086 --- /dev/null +++ b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataEclassCatalog.cs @@ -0,0 +1,46 @@ +namespace FluentAAS.Templates.TechnicalData; + +internal static class TechnicalDataEclassCatalog +{ + private static readonly IReadOnlyDictionary DefinitionsByIdShort = + new Dictionary(StringComparer.Ordinal) + { + [TechnicalDataIdentifiers.RatedVoltage] = new PropertyDefinition(TechnicalDataIdentifiers.RatedVoltage, TechnicalDataIdentifiers.MotorPerformance, TechnicalDataSemantics.RatedVoltage, DataTypeDefXsd.Decimal, true, ["V"]), + [TechnicalDataIdentifiers.RatedCurrent] = new PropertyDefinition(TechnicalDataIdentifiers.RatedCurrent, TechnicalDataIdentifiers.MotorPerformance, TechnicalDataSemantics.RatedCurrent, DataTypeDefXsd.Decimal, true, ["A"]), + [TechnicalDataIdentifiers.RatedPower] = new PropertyDefinition(TechnicalDataIdentifiers.RatedPower, TechnicalDataIdentifiers.MotorPerformance, TechnicalDataSemantics.RatedPower, DataTypeDefXsd.Decimal, true, ["kW", "W"]), + [TechnicalDataIdentifiers.RatedSpeed] = new PropertyDefinition(TechnicalDataIdentifiers.RatedSpeed, TechnicalDataIdentifiers.MotorPerformance, TechnicalDataSemantics.RatedSpeed, DataTypeDefXsd.Decimal, true, ["1/min", "rpm"]), + + [TechnicalDataIdentifiers.InnerDiameter] = new PropertyDefinition(TechnicalDataIdentifiers.InnerDiameter, TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataSemantics.InnerDiameter, DataTypeDefXsd.Decimal, true, ["mm"]), + [TechnicalDataIdentifiers.OuterDiameter] = new PropertyDefinition(TechnicalDataIdentifiers.OuterDiameter, TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataSemantics.OuterDiameter, DataTypeDefXsd.Decimal, true, ["mm"]), + [TechnicalDataIdentifiers.Width] = new PropertyDefinition(TechnicalDataIdentifiers.Width, TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataSemantics.Width, DataTypeDefXsd.Decimal, true, ["mm"]), + [TechnicalDataIdentifiers.LimitingSpeed] = new PropertyDefinition(TechnicalDataIdentifiers.LimitingSpeed, TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataSemantics.LimitingSpeed, DataTypeDefXsd.Decimal, true, ["1/min", "rpm"]) + }; + + public static PropertyDefinition ForIdShort(string idShort) + { + if (!DefinitionsByIdShort.TryGetValue(idShort, out var definition)) + { + throw new InvalidOperationException($"No ECLASS catalog mapping exists for technical property '{idShort}'."); + } + + return definition; + } + + public static IReadOnlyCollection RequiredInGroup(string groupIdShort) + { + return DefinitionsByIdShort.Values.Where(x => x.GroupIdShort == groupIdShort && x.IsRequired).ToArray(); + } + + public static bool IsKnownEclassReference(string eclassIrdi) + { + return DefinitionsByIdShort.Values.Any(x => string.Equals(x.EclassIrdi, eclassIrdi, StringComparison.Ordinal)); + } + + internal sealed record PropertyDefinition( + string IdShort, + string GroupIdShort, + string EclassIrdi, + DataTypeDefXsd ValueType, + bool IsRequired, + IReadOnlyCollection AllowedUnits); +} diff --git a/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataIdentifiers.cs b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataIdentifiers.cs new file mode 100644 index 0000000..30e72b1 --- /dev/null +++ b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataIdentifiers.cs @@ -0,0 +1,58 @@ +namespace FluentAAS.Templates.TechnicalData; + +/// +/// Provides the canonical idShort values used by the Technical Data builder. +/// Reusing these identifiers keeps generated structures consistent across projects. +/// +public static class TechnicalDataIdentifiers +{ + /// + /// Group identifier for motor performance parameters. + /// + public const string MotorPerformance = "MotorPerformance"; + + /// + /// Group identifier for bearing characteristics. + /// + public const string BearingCharacteristics = "BearingCharacteristics"; + + /// + /// Identifier for rated voltage. + /// + public const string RatedVoltage = "RatedVoltage"; + + /// + /// Identifier for rated current. + /// + public const string RatedCurrent = "RatedCurrent"; + + /// + /// Identifier for rated power. + /// + public const string RatedPower = "RatedPower"; + + /// + /// Identifier for rated speed. + /// + public const string RatedSpeed = "RatedSpeed"; + + /// + /// Identifier for bearing inner diameter. + /// + public const string InnerDiameter = "InnerDiameter"; + + /// + /// Identifier for bearing outer diameter. + /// + public const string OuterDiameter = "OuterDiameter"; + + /// + /// Identifier for bearing width. + /// + public const string Width = "Width"; + + /// + /// Identifier for bearing limiting speed. + /// + public const string LimitingSpeed = "LimitingSpeed"; +} diff --git a/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataSemantics.cs b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataSemantics.cs new file mode 100644 index 0000000..995bd8a --- /dev/null +++ b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataSemantics.cs @@ -0,0 +1,54 @@ +namespace FluentAAS.Templates.TechnicalData; + +/// +/// Provides stable semantic identifiers for the Technical Data template. +/// Using these constants helps avoid typos and ensures that created AAS elements +/// are linked to the intended ECLASS/IDTA semantics. +/// +public static class TechnicalDataSemantics +{ + /// + /// Semantic identifier of the IDTA Technical Data submodel template (v3.0). + /// + public const string SubmodelTechnicalData = "https://admin-shell.io/idta/TechnicalData/SubmodelTemplate/3/0"; + + /// + /// ECLASS IRDI for rated voltage. + /// + public const string RatedVoltage = "0173-1#02-BAF053#008"; + + /// + /// ECLASS IRDI for rated current. + /// + public const string RatedCurrent = "0173-1#02-BAF054#008"; + + /// + /// ECLASS IRDI for rated power. + /// + public const string RatedPower = "0173-1#02-BAF055#008"; + + /// + /// ECLASS IRDI for rated speed. + /// + public const string RatedSpeed = "0173-1#02-BAF056#008"; + + /// + /// ECLASS IRDI for bearing inner diameter. + /// + public const string InnerDiameter = "0173-1#02-AAO677#002"; + + /// + /// ECLASS IRDI for bearing outer diameter. + /// + public const string OuterDiameter = "0173-1#02-AAO678#002"; + + /// + /// ECLASS IRDI for bearing width. + /// + public const string Width = "0173-1#02-AAO679#002"; + + /// + /// ECLASS IRDI for bearing limiting speed. + /// + public const string LimitingSpeed = "0173-1#02-AAO680#002"; +} diff --git a/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataShellBuilderExtensions.cs b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataShellBuilderExtensions.cs new file mode 100644 index 0000000..1919982 --- /dev/null +++ b/FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataShellBuilderExtensions.cs @@ -0,0 +1,30 @@ +using FluentAAS.Builder; + +namespace FluentAAS.Templates.TechnicalData; + +/// +/// Extension methods to start Technical Data composition directly from a shell builder. +/// This keeps Technical Data authoring aligned with existing FluentAAS composition workflows. +/// +public static class TechnicalDataShellBuilderExtensions +{ + /// + /// Starts a for the current shell. + /// Callers get a focused fluent API that validates technical parameters before they are attached. + /// + /// The shell that will receive the Technical Data submodel. + /// Unique identifier of the Technical Data submodel. + /// Short readable name of the Technical Data submodel. + /// A specialized technical data builder. + public static TechnicalDataBuilder AddTechnicalData(this IShellBuilder shellBuilder, string id, string idShort = "TechnicalData") + { + ArgumentNullException.ThrowIfNull(shellBuilder); + + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Submodel id must not be empty.", nameof(id)); + } + + return new TechnicalDataBuilder(shellBuilder, id, idShort); + } +} diff --git a/FluentAAS/FluentAASTests.Integration/FluentAasEndToEndTests.cs b/FluentAAS/FluentAASTests.Integration/FluentAasEndToEndTests.cs index 145414d..6f5c8ea 100644 --- a/FluentAAS/FluentAASTests.Integration/FluentAasEndToEndTests.cs +++ b/FluentAAS/FluentAASTests.Integration/FluentAasEndToEndTests.cs @@ -7,6 +7,7 @@ using FluentAAS.Templates.DigitalNameplate; using FluentAAS.Templates.HandoverDocumentation; using FluentAAS.Templates.HandoverDocumentation.Document; + using FluentAAS.Templates.TechnicalData; using Shouldly; using File = System.IO.File; @@ -155,6 +156,35 @@ public void CanCreateHandoverDocumentationWithFluentApi() AssertDocumentContent(environment); } + /// + /// Verifies that the Technical Data template can be composed as part of a full environment + /// and persists structured motor and bearing parameters with expected ECLASS semantics. + /// + [Fact] + public void CanCreateTechnicalDataWithFluentApi() + { + var environment = TechnicalDataCompositionExample.BuildMotorAndBearingExampleEnvironment(); + + environment.AssetAdministrationShells.ShouldNotBeNull(); + environment.AssetAdministrationShells!.Count.ShouldBe(1); + environment.Submodels.ShouldNotBeNull(); + environment.Submodels!.Count.ShouldBe(1); + + var technicalData = environment.Submodels!.Single(sm => sm.Id == "urn:aas:submodel:technical-data:drivetrain:1000"); + technicalData.SemanticId!.Keys.Single().Value.ShouldBe(TechnicalDataSemantics.SubmodelTechnicalData); + + var motorPerformance = technicalData.SubmodelElements! + .OfType() + .Single(x => x.IdShort == TechnicalDataIdentifiers.MotorPerformance); + var ratedVoltage = motorPerformance.Value! + .OfType() + .Single(x => x.IdShort == TechnicalDataIdentifiers.RatedVoltage); + + ratedVoltage.Value.ShouldBe("400"); + ratedVoltage.SemanticId!.Keys.Single().Value.ShouldBe(TechnicalDataSemantics.RatedVoltage); + ratedVoltage.Category.ShouldBe("V"); + } + private static IEnvironment CreateHandoverDocumentationEnvironment(DateTime testDate) { return AasBuilder.Create() @@ -413,4 +443,4 @@ public void HandoverDocumentationBuilder_ThrowsOnMissingRequiredDocuments() .OfType() .FirstOrDefault(p => p.IdShort == idShort); } - } \ No newline at end of file + } diff --git a/FluentAAS/FluentAASTests.Integration/TechnicalDataCompositionExample.cs b/FluentAAS/FluentAASTests.Integration/TechnicalDataCompositionExample.cs new file mode 100644 index 0000000..8594c59 --- /dev/null +++ b/FluentAAS/FluentAASTests.Integration/TechnicalDataCompositionExample.cs @@ -0,0 +1,37 @@ +using AasCore.Aas3_0; +using FluentAAS.Builder; +using FluentAAS.Templates.TechnicalData; + +namespace FluentAASTests.Integration; + +/// +/// Test-only composition sample for the Technical Data template. +/// Kept in the integration test project so production packages stay free of example-only code. +/// +public static class TechnicalDataCompositionExample +{ + /// + /// Builds an example environment containing one shell and one technical data submodel. + /// + /// A fully built environment used by integration tests and documentation validation. + public static IEnvironment BuildMotorAndBearingExampleEnvironment() + { + var aasBuilder = AasBuilder.Create(); + + aasBuilder.AddShell("urn:aas:example:drivetrain:1000", "DriveTrainAAS") + .AddTechnicalData("urn:aas:submodel:technical-data:drivetrain:1000") + .WithMotorPerformance(motor => motor + .WithRatedVoltage(400m, "V", TechnicalDataSemantics.RatedVoltage) + .WithRatedCurrent(82.5m, "A", TechnicalDataSemantics.RatedCurrent) + .WithRatedPower(45m, "kW", TechnicalDataSemantics.RatedPower) + .WithRatedSpeed(1470m, "1/min", TechnicalDataSemantics.RatedSpeed)) + .WithBearingCharacteristics(bearing => bearing + .WithInnerDiameter(50m, "mm", TechnicalDataSemantics.InnerDiameter) + .WithOuterDiameter(90m, "mm", TechnicalDataSemantics.OuterDiameter) + .WithWidth(20m, "mm", TechnicalDataSemantics.Width) + .WithLimitingSpeed(6500m, "1/min", TechnicalDataSemantics.LimitingSpeed)) + .BuildTechnicalData(); + + return aasBuilder.Build(); + } +} diff --git a/README.md b/README.md index b6c2ec5..1a44f74 100644 --- a/README.md +++ b/README.md @@ -141,12 +141,33 @@ Use these when your submodel matches an official IDTA template. The builder enfo **Currently supported:** - Digital Nameplate V2.0 (IDTA 02006-2-0) +- Technical Data (motor performance + bearing characteristics) **Planned:** - Handover Documentation -- Technical Data - Digital Product Passport +### Technical Data Template Example + +```csharp +var environment = AasBuilder.Create() + .AddShell("urn:aas:example:drivetrain:1000", "DriveTrainAAS") + .AddTechnicalData("urn:aas:submodel:technical-data:drivetrain:1000") + .WithMotorPerformance(motor => motor + .WithRatedVoltage(400m, "V", TechnicalDataSemantics.RatedVoltage) + .WithRatedCurrent(82.5m, "A", TechnicalDataSemantics.RatedCurrent) + .WithRatedPower(45m, "kW", TechnicalDataSemantics.RatedPower) + .WithRatedSpeed(1470m, "1/min", TechnicalDataSemantics.RatedSpeed)) + .WithBearingCharacteristics(bearing => bearing + .WithInnerDiameter(50m, "mm", TechnicalDataSemantics.InnerDiameter) + .WithOuterDiameter(90m, "mm", TechnicalDataSemantics.OuterDiameter) + .WithWidth(20m, "mm", TechnicalDataSemantics.Width) + .WithLimitingSpeed(6500m, "1/min", TechnicalDataSemantics.LimitingSpeed)) + .BuildTechnicalData() + .CompleteShellConfiguration() + .Build(); +``` + ### 2. Generic Builder For custom submodels or templates not yet supported: