-
Notifications
You must be signed in to change notification settings - Fork 0
Add TechnicalDataBuilder with ECLASS/IEC-61360 validation and Template Integration #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a7c7dd0
a99d976
8bff9fa
94e4c27
fa95a87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| 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<InvalidOperationException>(); | ||
| 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<InvalidOperationException>(); | ||
| 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<InvalidOperationException>(); | ||
| 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<SubmodelElementCollection>() | ||
| .Single(x => x.IdShort == TechnicalDataIdentifiers.MotorPerformance); | ||
|
|
||
| var ratedVoltage = motorGroup.Value! | ||
| .OfType<Property>() | ||
| .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 CompositionExample_ShouldBuildEnvironment() | ||
| { | ||
| var env = TechnicalDataCompositionExample.BuildMotorAndBearingExampleEnvironment(); | ||
|
|
||
| env.AssetAdministrationShells.ShouldNotBeNull(); | ||
| env.AssetAdministrationShells!.Count.ShouldBe(1); | ||
| env.Submodels.ShouldNotBeNull(); | ||
| env.Submodels!.Count.ShouldBe(1); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| using FluentAAS.Builder; | ||
| using FluentAAS.Builder.SubModel; | ||
|
|
||
| namespace FluentAAS.Templates.TechnicalData; | ||
|
|
||
| public sealed class TechnicalDataBuilder | ||
| { | ||
| private readonly IShellBuilder _shellBuilder; | ||
| private string _id; | ||
| private string _idShort; | ||
|
|
||
| private readonly Dictionary<string, GroupAssignment> _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; | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| public TechnicalDataBuilder WithMotorPerformance(Action<MotorPerformanceGroupBuilder> configure) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(configure); | ||
|
|
||
| var groupBuilder = new MotorPerformanceGroupBuilder(this); | ||
| configure(groupBuilder); | ||
|
|
||
| return this; | ||
| } | ||
|
|
||
| public TechnicalDataBuilder WithBearingCharacteristics(Action<BearingCharacteristicsGroupBuilder> configure) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(configure); | ||
|
|
||
| var groupBuilder = new BearingCharacteristicsGroupBuilder(this); | ||
| configure(groupBuilder); | ||
|
|
||
| return this; | ||
| } | ||
|
|
||
| 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 | ||
| }; | ||
|
Comment on lines
+121
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The IDTA Technical Data submodel template is the "Generic Frame for Technical Data for Industrial Equipment in Manufacturing" (IDTA number 02003, version 2.0), published by the Industrial Digital Twin Association (IDTA) for use with Asset Administration Shell (AAS) 3.0. It provides an interoperable structure for technical properties of assets, using semantic dictionaries like ECLASS and IEC CDD. Properties in this submodel are defined as AAS Property elements with semanticIds referencing external concept descriptions (e.g., IRDIs from ECLASS). For quantitative properties (e.g., dataType INTEGER_MEASURE, REAL_MEASURE), the unit is specified via the IEC 61360 data specification template (IDTA-01003-a), which includes attributes: - unit: string for unit in text (e.g., "m³" for storage volume). - unitId: Reference to a unit concept (e.g., IRDI for cubic meter). "Storage" likely refers to a property like storage capacity (e.g., ECLASS property for volume), where the value has a unit such as cubic meters (m³). Example structure: - Property idShort: "storageCapacity" - semanticId: IRDI (e.g., 0173-1#02-BAF188#007 for gross volume) - value: e.g., "10" - hasDataSpecification: DataSpecificationIec61360 with unit="m³", unitId=reference to unit concept. This follows AAS 3.0 metamodel (IDTA-01001), where units are part of embedded data specs for interoperability. Official spec downloadable from IDTA site; GitHub repo at admin-shell-io/submodel-templates for JSON/UML/AASX files. Citations:
🏁 Script executed: # First, find the Property class definition
find . -type f -name "*.cs" | head -20Repository: Freezor/FluentAAS Length of output: 1180 🏁 Script executed: # Search for Property class definition
rg "class Property" -A 10 --max-count 5Repository: Freezor/FluentAAS Length of output: 43 🏁 Script executed: # Look for IEC 61360 or DataSpecification references related to units
rg -i "iec.?61360|dataspecification|unit" --max-count 20Repository: Freezor/FluentAAS Length of output: 10802 🏁 Script executed: # Check if there are comments explaining Category usage in this file
cat -n FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataBuilder.cs | head -100Repository: Freezor/FluentAAS Length of output: 4192 🏁 Script executed: # Search for other Category assignments to see if this is a pattern
rg "Category\s*=" -B 2 -A 2 --max-count 10Repository: Freezor/FluentAAS Length of output: 2220 Store unit information using IEC 61360 data specification instead of the The Consider:
🤖 Prompt for AI Agents |
||
|
|
||
| collection.Value!.Add(property); | ||
| } | ||
|
|
||
| submodelBuilder.AddElement(collection); | ||
| } | ||
|
|
||
| var submodel = submodelBuilder.BuildSubmodel(); | ||
| _shellBuilder.AddSubmodelReference(submodel); | ||
|
|
||
| return _shellBuilder; | ||
| } | ||
|
|
||
| private void ValidateRequiredParameters() | ||
| { | ||
| var missing = new List<string>(); | ||
|
|
||
| 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<string, PropertyAssignment> Assignments { get; } = new(StringComparer.Ordinal); | ||
| } | ||
|
|
||
| private sealed record PropertyAssignment( | ||
| TechnicalDataEclassCatalog.PropertyDefinition Definition, | ||
| string Value, | ||
| string Unit); | ||
|
|
||
| public sealed class MotorPerformanceGroupBuilder(TechnicalDataBuilder parent) | ||
| { | ||
| public MotorPerformanceGroupBuilder WithRatedVoltage(decimal value, string unit = "V", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedVoltage, value, unit, semanticId); | ||
| return this; | ||
| } | ||
|
|
||
| public MotorPerformanceGroupBuilder WithRatedCurrent(decimal value, string unit = "A", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedCurrent, value, unit, semanticId); | ||
| return this; | ||
| } | ||
|
|
||
| public MotorPerformanceGroupBuilder WithRatedPower(decimal value, string unit = "kW", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedPower, value, unit, semanticId); | ||
| return this; | ||
| } | ||
|
|
||
| public MotorPerformanceGroupBuilder WithRatedSpeed(decimal value, string unit = "1/min", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.MotorPerformance, TechnicalDataIdentifiers.RatedSpeed, value, unit, semanticId); | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| public sealed class BearingCharacteristicsGroupBuilder(TechnicalDataBuilder parent) | ||
| { | ||
| public BearingCharacteristicsGroupBuilder WithInnerDiameter(decimal value, string unit = "mm", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.InnerDiameter, value, unit, semanticId); | ||
| return this; | ||
| } | ||
|
|
||
| public BearingCharacteristicsGroupBuilder WithOuterDiameter(decimal value, string unit = "mm", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.OuterDiameter, value, unit, semanticId); | ||
| return this; | ||
| } | ||
|
|
||
| public BearingCharacteristicsGroupBuilder WithWidth(decimal value, string unit = "mm", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.Width, value, unit, semanticId); | ||
| return this; | ||
| } | ||
|
|
||
| public BearingCharacteristicsGroupBuilder WithLimitingSpeed(decimal value, string unit = "1/min", string? semanticId = null) | ||
| { | ||
| parent.UpsertDecimalProperty(TechnicalDataIdentifiers.BearingCharacteristics, TechnicalDataIdentifiers.LimitingSpeed, value, unit, semanticId); | ||
| return this; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| using FluentAAS.Builder; | ||
|
|
||
| namespace FluentAAS.Templates.TechnicalData; | ||
|
|
||
| public static class TechnicalDataCompositionExample | ||
| { | ||
| public static IEnvironment BuildMotorAndBearingExampleEnvironment() | ||
| { | ||
| return 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() | ||
| .Build(); | ||
|
Check failure on line 23 in FluentAAS/FluentAAS.Templates/TechnicalData/TechnicalDataCompositionExample.cs
|
||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Freezor/FluentAAS
Length of output: 3003
🌐 Web query:
AAS 3.0 Property Category field usage and IEC 61360 unit storage💡 Result:
In AAS 3.0, the 'category' field is defined in the Referable abstract class, inherited by elements like Property. It provides meta-information about the element's class, affecting expected attributes and constraints (e.g., distinguishing measurement values). It is optional (string type, default null) and deprecated since V3.0, though still present for backward compatibility. Legacy values include PARAMETER, CONSTANT, VALUE for data elements like Properties, influencing validation (e.g., in ConceptDescription constraints referencing category, marked deprecated). IEC 61360 units for quantitative properties (e.g., dataType INTEGER_MEASURE, REAL_MEASURE) are stored in ConceptDescriptions via the DataSpecificationIec61360 template, using 'unit' (string, primary unit text) or 'unitId' (Reference to global unit identifier, e.g., IRDI). These are embedded in the semanticId-referenced ConceptDescription of the Property, not directly in the Property.value. Constraint AASc-3a-009 requires unit/unitId for measure types. The Property.valueType maps IEC data types to XSD (e.g., xs:integer for INTEGER_MEASURE).
Citations:
Remove Category field usage for unit storage; use ConceptDescription's DataSpecificationIec61360 instead.
The test asserts
ratedVoltage.Category.ShouldBe("V"), but in AAS 3.0, theCategoryfield is deprecated and meant for element classification (e.g., PARAMETER, CONSTANT, VALUE). Units should be stored in the ConceptDescription'sDataSpecificationIec61360template via theunitfield, not in the Property'sCategoryfield. This applies to the builder implementation as well (TechnicalDataBuilder.cs:95).🤖 Prompt for AI Agents