From a04ebc57471eeff6c82b8edadd877ade2cfeebc2 Mon Sep 17 00:00:00 2001 From: "Ronan F." Date: Sun, 16 Aug 2026 14:51:33 +0200 Subject: [PATCH 1/3] test(csharp,python): add failing tests demonstrating the MqttLocalId LSP violation Adds a failing test per language simulating a real-world function signature that accepts a LocalId parameter (exactly what happens when an MqttLocalId is passed as an argument, since it IS-A LocalId): the returned string is the MQTT format instead of the standard dnv-v2 format. --- csharp/test/Vista.SDK.Tests/LocalIdTests.cs | 26 ++++++++++++++++ python/tests/test_local_id.py | 34 +++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/csharp/test/Vista.SDK.Tests/LocalIdTests.cs b/csharp/test/Vista.SDK.Tests/LocalIdTests.cs index 6ebca0c6c..7a3e2f186 100644 --- a/csharp/test/Vista.SDK.Tests/LocalIdTests.cs +++ b/csharp/test/Vista.SDK.Tests/LocalIdTests.cs @@ -114,6 +114,32 @@ public sealed record class Input( }, }; + // Simulates any real-world function signature that accepts a LocalId parameter -- this is + // exactly what happens when an MqttLocalId is passed as an argument, since it IS-A LocalId. + static string DescribeLocalId(LocalId id) => id.ToString(); + + [Fact] + public void Test_LSP_Violation_MqttLocalId_Passed_As_LocalId_Parameter_Produces_Standard_Format() + { + var (_, vis) = VISTests.GetVis(); + var visVersion = VisVersion.v3_4a; + var gmod = vis.GetGmod(visVersion); + var codebooks = vis.GetCodebooks(visVersion); + + var primaryItem = gmod.ParsePath("411.1/C101.31-2"); + var qtyTag = codebooks.TryCreateTag(CodebookName.Quantity, "temperature"); + + var builder = LocalIdBuilder.Create(visVersion).TryWithPrimaryItem(primaryItem).TryWithMetadataTag(qtyTag); + + var mqttLocalId = builder.BuildMqtt(); + + // Compiles fine: mqttLocalId IS-A LocalId, no cast needed. + var described = DescribeLocalId(mqttLocalId); + + // This assertion fails: DescribeLocalId returns the MQTT format, not the standard format. + described.Should().StartWith("/dnv-v2/"); + } + [Theory] [MemberData(nameof(Valid_Test_Data))] public void Test_LocalId_Build_Valid(Input input, string expectedOutput) diff --git a/python/tests/test_local_id.py b/python/tests/test_local_id.py index 9c84b0614..af3758569 100644 --- a/python/tests/test_local_id.py +++ b/python/tests/test_local_id.py @@ -271,6 +271,40 @@ def test_mqtt_local_id_build_valid( local_id_str = str(mqtt_local_id) assert expected_output == local_id_str + def test_lsp_violation_mqtt_local_id_passed_as_local_id_parameter_produces_standard_format( + self, + ) -> None: + """Simulates any real-world function signature that accepts a LocalId parameter. + + This is exactly what happens when an MqttLocalId is passed as an argument, since + MqttLocalId is a subclass of LocalId. + """ + + def describe_local_id(local_id: LocalId) -> str: + return str(local_id) + + vis = VIS() + vis_version = VisVersion.v3_4a + gmod = vis.get_gmod(vis_version) + codebooks = vis.get_codebooks(vis_version) + + primary_item = gmod.parse_path("411.1/C101.31-2") + qty_tag = codebooks.try_create_tag(CodebookName.Quantity, "temperature") + + builder = ( + LocalIdBuilder.create(vis_version) + .try_with_primary_item(primary_item) + .try_with_metadata_tag(qty_tag) + ) + + mqtt_local_id = MqttLocalId(builder) + + # No cast needed: mqtt_local_id is a LocalId instance. + described = describe_local_id(mqtt_local_id) + + # This assertion fails: describe_local_id returns the MQTT format, not the standard format. + assert described.startswith("/dnv-v2/") + @pytest.mark.parametrize( "input_data", [ From 734b78e36db460b50621ebd4b9dd9e74a757e128 Mon Sep 17 00:00:00 2001 From: "Ronan F." Date: Sun, 16 Aug 2026 18:02:18 +0200 Subject: [PATCH 2/3] fix(csharp,python)!: compose LocalIdBuilder in MqttLocalId instead of inheriting LocalId BREAKING CHANGE: MqttLocalId no longer inherits from LocalId (C#) / is no longer a subclass of LocalId (Python). Code that upcasts MqttLocalId to LocalId, or checks `is LocalId`/`isinstance(x, LocalId)`, will no longer compile (C#) or will start returning False (Python). MqttLocalId publicly inherited from LocalId but produced a fundamentally incompatible ToString()/__str__() format (no leading slash, underscores, no meta/ section), violating Liskov substitution: a caller holding a LocalId reference could silently receive MQTT-topic text instead of the standard dnv-v2 format. MqttLocalId now composes a LocalIdBuilder directly. NamingRule/NAMING_RULE and members unused by the MQTT format (VerboseMode/HasCustomTag/MetadataTags) are reachable via .Builder/.builder instead of being duplicated. --- csharp/src/Vista.SDK.Mqtt/MqttLocalId.cs | 63 ++++++++++++- csharp/test/Vista.SDK.Tests/LocalIdTests.cs | 99 +++++++++++++++++++-- python/src/vista_sdk/mqtt/mqtt_local_id.py | 91 ++++++++++++++++++- python/tests/test_local_id.py | 92 ++++++++++++++++--- 4 files changed, 319 insertions(+), 26 deletions(-) diff --git a/csharp/src/Vista.SDK.Mqtt/MqttLocalId.cs b/csharp/src/Vista.SDK.Mqtt/MqttLocalId.cs index 58e6b413c..5612a5b91 100644 --- a/csharp/src/Vista.SDK.Mqtt/MqttLocalId.cs +++ b/csharp/src/Vista.SDK.Mqtt/MqttLocalId.cs @@ -8,16 +8,73 @@ public static class LocalIdBuilderExtensions public static MqttLocalId BuildMqtt(this LocalIdBuilder builder) => new MqttLocalId(builder); } -public class MqttLocalId : LocalId +public class MqttLocalId : IEquatable { private static readonly char _internal_separator = '_'; + private readonly LocalIdBuilder _builder; + public MqttLocalId(LocalIdBuilder builder) - : base(builder) { } + { + if (builder.IsEmpty) + throw new ArgumentException("LocalId cannot be constructed from empty LocalIdBuilder"); + if (!builder.IsValid) + throw new ArgumentException("LocalId cannot be constructed from invalid LocalIdBuilder"); + _builder = builder; + } + + public LocalIdBuilder Builder => _builder; + + public VisVersion VisVersion => _builder.VisVersion!.Value; + + public GmodPath PrimaryItem => _builder.PrimaryItem!; + + public GmodPath? SecondaryItem => _builder.SecondaryItem; + + public MetadataTag? Quantity => _builder.Quantity; + + public MetadataTag? Content => _builder.Content; + + public MetadataTag? Calculation => _builder.Calculation; + + public MetadataTag? State => _builder.State; + + public MetadataTag? Command => _builder.Command; + + public MetadataTag? Type => _builder.Type; + + public MetadataTag? Position => _builder.Position; + + public MetadataTag? Detail => _builder.Detail; + + public sealed override bool Equals(object? obj) => Equals(obj as MqttLocalId); + + public bool Equals(MqttLocalId? other) + { + if (other is null) + return false; + + return _builder.Equals(other._builder); + } + + public static bool operator !=(MqttLocalId? left, MqttLocalId? right) => !(left == right); + + public static bool operator ==(MqttLocalId? left, MqttLocalId? right) + { + if (!ReferenceEquals(left, right)) + { + if (left is not null) + return left.Equals(right); + return false; + } + return true; + } + + public sealed override int GetHashCode() => _builder.GetHashCode(); public override string ToString() { - string namingRule = $"{NamingRule}/"; + string namingRule = $"{LocalId.NamingRule}/"; using var lease = StringBuilderPool.Get(); var builder = lease.Builder; diff --git a/csharp/test/Vista.SDK.Tests/LocalIdTests.cs b/csharp/test/Vista.SDK.Tests/LocalIdTests.cs index 7a3e2f186..91e54c12b 100644 --- a/csharp/test/Vista.SDK.Tests/LocalIdTests.cs +++ b/csharp/test/Vista.SDK.Tests/LocalIdTests.cs @@ -114,12 +114,15 @@ public sealed record class Input( }, }; - // Simulates any real-world function signature that accepts a LocalId parameter -- this is - // exactly what happens when an MqttLocalId is passed as an argument, since it IS-A LocalId. - static string DescribeLocalId(LocalId id) => id.ToString(); + [Fact] + public void Test_MqttLocalId_Is_Not_A_LocalId_The_Former_LSP_Violation_Is_Now_Impossible_By_Construction() + { + // MqttLocalId must not inherit from LocalId + typeof(LocalId).IsAssignableFrom(typeof(MqttLocalId)).Should().BeFalse(); + } [Fact] - public void Test_LSP_Violation_MqttLocalId_Passed_As_LocalId_Parameter_Produces_Standard_Format() + public void Test_MqttLocalId_Getters() { var (_, vis) = VISTests.GetVis(); var visVersion = VisVersion.v3_4a; @@ -127,17 +130,95 @@ public void Test_LSP_Violation_MqttLocalId_Passed_As_LocalId_Parameter_Produces_ var codebooks = vis.GetCodebooks(visVersion); var primaryItem = gmod.ParsePath("411.1/C101.31-2"); + var secondaryItem = gmod.ParsePath("411.1/C101.31-5"); var qtyTag = codebooks.TryCreateTag(CodebookName.Quantity, "temperature"); + var cntTag = codebooks.TryCreateTag(CodebookName.Content, "exhaust.gas"); - var builder = LocalIdBuilder.Create(visVersion).TryWithPrimaryItem(primaryItem).TryWithMetadataTag(qtyTag); + var builder = LocalIdBuilder + .Create(visVersion) + .TryWithPrimaryItem(primaryItem) + .TryWithSecondaryItem(secondaryItem) + .TryWithMetadataTag(qtyTag) + .TryWithMetadataTag(cntTag); + + var mqttLocalId = builder.BuildMqtt(); + + mqttLocalId.VisVersion.Should().Be(visVersion); + mqttLocalId.PrimaryItem.Should().Be(primaryItem); + mqttLocalId.SecondaryItem.Should().Be(secondaryItem); + mqttLocalId.Quantity.Should().Be(qtyTag); + mqttLocalId.Content.Should().Be(cntTag); + mqttLocalId.Calculation.Should().BeNull(); + mqttLocalId.State.Should().BeNull(); + mqttLocalId.Command.Should().BeNull(); + mqttLocalId.Type.Should().BeNull(); + mqttLocalId.Position.Should().BeNull(); + mqttLocalId.Detail.Should().BeNull(); + } + + [Fact] + public void Test_MqttLocalId_Builder_Exposes_Members_Not_Reflected_In_The_Mqtt_Format() + { + // VerboseMode/HasCustomTag/MetadataTags have no effect on MqttLocalId.ToString() (the MQTT + // format never reads them), so MqttLocalId does not duplicate them as its own properties - + // they remain reachable via .Builder, same as any other LocalIdBuilder-derived information. + var (_, vis) = VISTests.GetVis(); + var visVersion = VisVersion.v3_4a; + var gmod = vis.GetGmod(visVersion); + var codebooks = vis.GetCodebooks(visVersion); + + var primaryItem = gmod.ParsePath("411.1/C101.63/S206"); + var qtyTag = codebooks.TryCreateTag(CodebookName.Quantity, "temperature"); + + var builder = LocalIdBuilder + .Create(visVersion) + .WithVerboseMode(true) + .TryWithPrimaryItem(primaryItem) + .TryWithMetadataTag(qtyTag); var mqttLocalId = builder.BuildMqtt(); - // Compiles fine: mqttLocalId IS-A LocalId, no cast needed. - var described = DescribeLocalId(mqttLocalId); + mqttLocalId.Builder.VerboseMode.Should().BeTrue(); + mqttLocalId.Builder.HasCustomTag.Should().BeFalse(); + mqttLocalId.Builder.MetadataTags.Should().ContainSingle(); + + // Confirms VerboseMode has no effect on the MQTT format itself: no '~' common-name segments + // appear even though verbose mode is on and this same primary item DOES produce '~' segments + // in the standard LocalId format (see Valid_Test_Data's "411.1/C101.63/S206" verbose case). + mqttLocalId.ToString().Should().NotContain("~"); + } + + [Fact] + public void Test_MqttLocalId_Equality() + { + var (_, vis) = VISTests.GetVis(); + var visVersion = VisVersion.v3_4a; + var gmod = vis.GetGmod(visVersion); + var codebooks = vis.GetCodebooks(visVersion); + + var primaryItem = gmod.ParsePath("411.1/C101.31-2"); + var qtyTag = codebooks.TryCreateTag(CodebookName.Quantity, "temperature"); + + var builder = LocalIdBuilder.Create(visVersion).TryWithPrimaryItem(primaryItem).TryWithMetadataTag(qtyTag); + + var a = builder.BuildMqtt(); + var b = builder.BuildMqtt(); + + var otherTag = codebooks.TryCreateTag(CodebookName.Quantity, "pressure"); + var otherBuilder = LocalIdBuilder + .Create(visVersion) + .TryWithPrimaryItem(primaryItem) + .TryWithMetadataTag(otherTag); + var c = otherBuilder.BuildMqtt(); + + a.Should().Be(b); + (a == b).Should().BeTrue(); + (a != b).Should().BeFalse(); + a.GetHashCode().Should().Be(b.GetHashCode()); - // This assertion fails: DescribeLocalId returns the MQTT format, not the standard format. - described.Should().StartWith("/dnv-v2/"); + a.Should().NotBe(c); + (a == c).Should().BeFalse(); + (a != c).Should().BeTrue(); } [Theory] diff --git a/python/src/vista_sdk/mqtt/mqtt_local_id.py b/python/src/vista_sdk/mqtt/mqtt_local_id.py index ec0bda2ec..0eb8ada03 100644 --- a/python/src/vista_sdk/mqtt/mqtt_local_id.py +++ b/python/src/vista_sdk/mqtt/mqtt_local_id.py @@ -12,12 +12,13 @@ from vista_sdk.gmod_path import GmodPath from vista_sdk.local_id import LocalId from vista_sdk.metadata_tag import MetadataTag +from vista_sdk.vis_version import VisVersion if TYPE_CHECKING: from vista_sdk.local_id_builder import LocalIdBuilder -class MqttLocalId(LocalId): +class MqttLocalId: """MQTT-formatted version of the LocalId class. This class provides a version of LocalId that formats the string representation @@ -27,13 +28,99 @@ class MqttLocalId(LocalId): # Class variables _internal_separator = "_" + NAMING_RULE = LocalId.NAMING_RULE + def __init__(self, builder: LocalIdBuilder) -> None: """Initialize a new MqttLocalId from a LocalIdBuilder. Args: builder: The LocalIdBuilder to create the MqttLocalId from + + Raises: + ValueError: If the builder is empty or invalid. """ - super().__init__(builder) + if builder.is_empty: + raise ValueError("LocalId cannot be constructed from empty LocalIdBuilder") + if not builder.is_valid: + raise ValueError( + "LocalId cannot be constructed from invalid LocalIdBuilder" + ) + self._builder = builder + + @property + def builder(self) -> LocalIdBuilder: + """Get the builder for this MqttLocalId.""" + return self._builder + + @property + def vis_version(self) -> VisVersion: + """Get the VIS version for this MqttLocalId.""" + return self._builder.vis_version # type: ignore + + @property + def primary_item(self) -> GmodPath: + """Get the primary item for this MqttLocalId.""" + return self._builder.primary_item # type: ignore + + @property + def secondary_item(self) -> GmodPath | None: + """Get the secondary item for this MqttLocalId.""" + return self._builder.secondary_item + + @property + def quantity(self) -> MetadataTag | None: + """Get the quantity metadata tag for this MqttLocalId.""" + return self._builder.quantity + + @property + def content(self) -> MetadataTag | None: + """Get the content metadata tag for this MqttLocalId.""" + return self._builder.content + + @property + def calculation(self) -> MetadataTag | None: + """Get the calculation metadata tag for this MqttLocalId.""" + return self._builder.calculation + + @property + def state(self) -> MetadataTag | None: + """Get the state metadata tag for this MqttLocalId.""" + return self._builder.state + + @property + def command(self) -> MetadataTag | None: + """Get the command metadata tag for this MqttLocalId.""" + return self._builder.command + + @property + def type(self) -> MetadataTag | None: + """Get the type metadata tag for this MqttLocalId.""" + return self._builder.type + + @property + def position(self) -> MetadataTag | None: + """Get the position metadata tag for this MqttLocalId.""" + return self._builder.position + + @property + def detail(self) -> MetadataTag | None: + """Get the detail metadata tag for this MqttLocalId.""" + return self._builder.detail + + def __eq__(self, other: object) -> bool: + """Compare this MqttLocalId with another for equality.""" + if not isinstance(other, MqttLocalId): + return False + + return self._builder == other._builder + + def __hash__(self) -> int: + """Get the hash code for this MqttLocalId.""" + return hash(self._builder) + + def __repr__(self) -> str: + """Get the official string representation of this MqttLocalId.""" + return f"MqttLocalId({self!s})" def __str__(self) -> str: """Get the string representation of the MqttLocalId. diff --git a/python/tests/test_local_id.py b/python/tests/test_local_id.py index af3758569..696863a6a 100644 --- a/python/tests/test_local_id.py +++ b/python/tests/test_local_id.py @@ -271,39 +271,107 @@ def test_mqtt_local_id_build_valid( local_id_str = str(mqtt_local_id) assert expected_output == local_id_str - def test_lsp_violation_mqtt_local_id_passed_as_local_id_parameter_produces_standard_format( + def test_mqtt_local_id_is_not_a_local_id_the_former_lsp_violation_is_now_impossible_by_construction( self, ) -> None: - """Simulates any real-world function signature that accepts a LocalId parameter. + # MqttLocalId must not inherit from LocalId + assert not issubclass(MqttLocalId, LocalId) - This is exactly what happens when an MqttLocalId is passed as an argument, since - MqttLocalId is a subclass of LocalId. - """ + def test_mqtt_local_id_getters(self) -> None: + vis = VIS() + vis_version = VisVersion.v3_4a + gmod = vis.get_gmod(vis_version) + codebooks = vis.get_codebooks(vis_version) + + primary_item = gmod.parse_path("411.1/C101.31-2") + secondary_item = gmod.parse_path("411.1/C101.31-5") + qty_tag = codebooks.try_create_tag(CodebookName.Quantity, "temperature") + cnt_tag = codebooks.try_create_tag(CodebookName.Content, "exhaust.gas") + + builder = ( + LocalIdBuilder.create(vis_version) + .try_with_primary_item(primary_item) + .try_with_secondary_item(secondary_item) + .try_with_metadata_tag(qty_tag) + .try_with_metadata_tag(cnt_tag) + ) - def describe_local_id(local_id: LocalId) -> str: - return str(local_id) + mqtt_local_id = MqttLocalId(builder) + assert mqtt_local_id.vis_version == vis_version + assert mqtt_local_id.primary_item == primary_item + assert mqtt_local_id.secondary_item == secondary_item + assert mqtt_local_id.quantity == qty_tag + assert mqtt_local_id.content == cnt_tag + assert mqtt_local_id.calculation is None + assert mqtt_local_id.state is None + assert mqtt_local_id.command is None + assert mqtt_local_id.type is None + assert mqtt_local_id.position is None + assert mqtt_local_id.detail is None + + def test_mqtt_local_id_builder_exposes_members_not_reflected_in_the_mqtt_format( + self, + ) -> None: + # verbose_mode/has_custom_tag/metadata_tags have no effect on MqttLocalId.__str__() (the + # MQTT format never reads them), so MqttLocalId does not duplicate them as its own + # properties - they remain reachable via .builder, same as any other LocalIdBuilder-derived + # information. vis = VIS() vis_version = VisVersion.v3_4a gmod = vis.get_gmod(vis_version) codebooks = vis.get_codebooks(vis_version) - primary_item = gmod.parse_path("411.1/C101.31-2") + primary_item = gmod.parse_path("411.1/C101.63/S206") qty_tag = codebooks.try_create_tag(CodebookName.Quantity, "temperature") builder = ( LocalIdBuilder.create(vis_version) + .with_verbose_mode(True) .try_with_primary_item(primary_item) .try_with_metadata_tag(qty_tag) ) mqtt_local_id = MqttLocalId(builder) - # No cast needed: mqtt_local_id is a LocalId instance. - described = describe_local_id(mqtt_local_id) + assert mqtt_local_id.builder.verbose_mode is True + assert mqtt_local_id.builder.has_custom_tag is False + assert len(mqtt_local_id.builder.metadata_tags) == 1 + + # Confirms verbose_mode has no effect on the MQTT format itself: no '~' common-name + # segments appear even though verbose mode is on and this same primary item DOES produce + # '~' segments in the standard LocalId format. + assert "~" not in str(mqtt_local_id) + + def test_mqtt_local_id_equality(self) -> None: + vis = VIS() + vis_version = VisVersion.v3_4a + gmod = vis.get_gmod(vis_version) + codebooks = vis.get_codebooks(vis_version) + + primary_item = gmod.parse_path("411.1/C101.31-2") + qty_tag = codebooks.try_create_tag(CodebookName.Quantity, "temperature") + + builder = ( + LocalIdBuilder.create(vis_version) + .try_with_primary_item(primary_item) + .try_with_metadata_tag(qty_tag) + ) + + a = MqttLocalId(builder) + b = MqttLocalId(builder) + + other_tag = codebooks.try_create_tag(CodebookName.Quantity, "pressure") + other_builder = ( + LocalIdBuilder.create(vis_version) + .try_with_primary_item(primary_item) + .try_with_metadata_tag(other_tag) + ) + c = MqttLocalId(other_builder) - # This assertion fails: describe_local_id returns the MQTT format, not the standard format. - assert described.startswith("/dnv-v2/") + assert a == b + assert hash(a) == hash(b) + assert a != c @pytest.mark.parametrize( "input_data", From 88e687366bc4ce1ab909e13329e434b0221df2c6 Mon Sep 17 00:00:00 2001 From: "Ronan F." Date: Sun, 16 Aug 2026 18:12:51 +0200 Subject: [PATCH 3/3] feat(csharp,python): add MQTT LocalId sections to AdvancedLocalIds sample --- .../AdvancedLocalIds/AdvancedLocalIds.csproj | 1 + csharp/samples/AdvancedLocalIds/Program.cs | 114 +++++++++++++++++ python/samples/advanced_local_ids.py | 120 ++++++++++++++++++ 3 files changed, 235 insertions(+) diff --git a/csharp/samples/AdvancedLocalIds/AdvancedLocalIds.csproj b/csharp/samples/AdvancedLocalIds/AdvancedLocalIds.csproj index 95417558e..a8ba555b9 100644 --- a/csharp/samples/AdvancedLocalIds/AdvancedLocalIds.csproj +++ b/csharp/samples/AdvancedLocalIds/AdvancedLocalIds.csproj @@ -7,6 +7,7 @@ + \ No newline at end of file diff --git a/csharp/samples/AdvancedLocalIds/Program.cs b/csharp/samples/AdvancedLocalIds/Program.cs index a64a5c872..293253f2a 100644 --- a/csharp/samples/AdvancedLocalIds/Program.cs +++ b/csharp/samples/AdvancedLocalIds/Program.cs @@ -8,6 +8,7 @@ Advanced Local ID Operations - Vista SDK C# - Local ID validation and inspection */ using Vista.SDK; +using Vista.SDK.Mqtt; Console.WriteLine("=== Advanced Local ID Operations ===\n"); @@ -179,4 +180,117 @@ Advanced Local ID Operations - Vista SDK C# Console.WriteLine($" Verbose mode: {verboseLocalId}"); Console.WriteLine($" Regular mode: {regularLocalId}"); +// 7. MQTT LocalId: formatting and components +Console.WriteLine("\n7. MQTT LocalId: MQTT-compatible formatting..."); + +string[] mqttExamples = +[ + "/dnv-v2/vis-3-4a/411.1/C101.31-2/meta/qty-temperature", + "/dnv-v2/vis-3-4a/411.1/C101.31-2/meta/qty-temperature/cnt-exhaust.gas/pos-inlet", + "/dnv-v2/vis-3-4a/621.21/S90/sec/411.1/C101/meta/qty-mass/cnt-fuel.oil/pos-inlet", +]; + +foreach (var localIdStr in mqttExamples) +{ + if (LocalId.TryParse(localIdStr, out _, out var parsedLocalId)) + { + var mqttLocalId = parsedLocalId.Builder.BuildMqtt(); + Console.WriteLine($" Standard : {parsedLocalId}"); + Console.WriteLine($" MQTT : {mqttLocalId}"); + Console.WriteLine(); + } +} + +// All 8 slots filled +var fullPrimary = gmod.ParsePath("411.1/C101.31-2"); +var fullSecondary = gmod.ParsePath("411.1/C101.31-5"); +var fullBuilder = LocalIdBuilder + .Create(version) + .WithPrimaryItem(fullPrimary) + .WithSecondaryItem(fullSecondary) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Content, "exhaust.gas")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Calculation, "average")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.State, "high")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Command, "start")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Type, "instantaneous")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Position, "inlet")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Detail, "my_sensor_42")); + +var fullStandard = fullBuilder.Build(); +var fullMqtt = fullBuilder.BuildMqtt(); + +Console.WriteLine(" All 8 slots + secondary + free-form detail:"); +Console.WriteLine($" Standard : {fullStandard}"); +Console.WriteLine($" MQTT : {fullMqtt}"); +Console.WriteLine($" Slots : qty/cnt/calc/state/cmd/type/pos/detail"); +Console.WriteLine($" Detail : '{fullStandard.Detail?.Value}' (IsCustom: {fullStandard.Detail?.IsCustom})"); +Console.WriteLine(); + +// Reading components +var compPrimary = gmod.ParsePath("411.1/C101.31-2"); +var compSecondary = gmod.ParsePath("411.1/C101.31-5"); +var compBuilder = LocalIdBuilder + .Create(version) + .WithPrimaryItem(compPrimary) + .WithSecondaryItem(compSecondary) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature")) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Content, "exhaust.gas")); + +var mqttComp = compBuilder.BuildMqtt(); + +Console.WriteLine(" Reading components:"); +Console.WriteLine($" VisVersion : {mqttComp.VisVersion.ToVersionString()}"); +Console.WriteLine($" PrimaryItem : {mqttComp.PrimaryItem}"); +Console.WriteLine($" SecondaryItem : {mqttComp.SecondaryItem}"); +Console.WriteLine($" Quantity : {mqttComp.Quantity}"); +Console.WriteLine($" Content : {mqttComp.Content}"); +Console.WriteLine($" Calculation : {mqttComp.Calculation?.ToString() ?? "(none)"}"); +Console.WriteLine(); + +// 8. MQTT LocalId: builder-level state and equality +Console.WriteLine("8. MQTT LocalId: builder-level state and equality..."); + +// Builder-level state not reflected in MQTT format +var verbosePrimary = gmod.ParsePath("411.1/C101.63/S206"); +var verboseBuilder = LocalIdBuilder + .Create(version) + .WithVerboseMode(true) + .WithPrimaryItem(verbosePrimary) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature")); + +var mqttVerbose = verboseBuilder.BuildMqtt(); + +Console.WriteLine(" Builder-level state (accessible via .Builder):"); +Console.WriteLine(" VerboseMode/HasCustomTag/MetadataTags are not duplicated on MqttLocalId."); +Console.WriteLine(" VerboseMode has no effect on the MQTT format (unlike LocalId.ToString())."); +Console.WriteLine($" Builder.VerboseMode : {mqttVerbose.Builder.VerboseMode}"); +Console.WriteLine($" Builder.HasCustomTag : {mqttVerbose.Builder.HasCustomTag}"); +Console.WriteLine($" Builder.MetadataTags : {mqttVerbose.Builder.MetadataTags.Count} tag(s)"); +Console.WriteLine($" MQTT (no '~' despite VerboseMode=true): {mqttVerbose}"); +Console.WriteLine(); + +// Equality +var eqPrimary = gmod.ParsePath("411.1/C101.31-2"); +var eqBuilder = LocalIdBuilder + .Create(version) + .WithPrimaryItem(eqPrimary) + .WithMetadataTag(codebooks.CreateTag(CodebookName.Quantity, "temperature")); + +var a = eqBuilder.BuildMqtt(); +var b = eqBuilder.BuildMqtt(); +var c = eqBuilder.WithMetadataTag(codebooks.CreateTag(CodebookName.Content, "exhaust.gas")).BuildMqtt(); + +Console.WriteLine(" Equality:"); +Console.WriteLine($" a == b (same builder) : {a == b}"); +Console.WriteLine($" a == c (extra tag) : {a == c}"); +Console.WriteLine(); + +Console.WriteLine(" MQTT format differences vs standard:"); +Console.WriteLine(" - No leading '/'"); +Console.WriteLine(" - Underscores instead of slashes in paths"); +Console.WriteLine(" - No 'meta/' section"); +Console.WriteLine(" - '_' placeholder for absent metadata slots"); +Console.WriteLine(" - 8 fixed slots: qty/cnt/calc/state/cmd/type/pos/detail"); + Console.WriteLine("\n=== Advanced operations completed! ==="); diff --git a/python/samples/advanced_local_ids.py b/python/samples/advanced_local_ids.py index f97ef48e8..3d4fc8138 100644 --- a/python/samples/advanced_local_ids.py +++ b/python/samples/advanced_local_ids.py @@ -12,6 +12,7 @@ from vista_sdk.gmod_path import GmodPath from vista_sdk.local_id_builder import LocalIdBuilder from vista_sdk.local_id_builder_parsing import LocalIdBuilderParsing +from vista_sdk.mqtt.mqtt_local_id import MqttLocalId from vista_sdk.vis import VIS from vista_sdk.vis_version import VisVersion @@ -187,6 +188,125 @@ def main() -> None: # noqa : C901 print(f" Verbose mode: {verbose_local_id}") print(f" Regular mode: {regular_local_id}") + # 7. MQTT LocalId: formatting and components + print("\n7. MQTT LocalId: MQTT-compatible formatting...") + + mqtt_examples = [ + "/dnv-v2/vis-3-4a/411.1/C101.31-2/meta/qty-temperature", + "/dnv-v2/vis-3-4a/411.1/C101.31-2/meta/qty-temperature/cnt-exhaust.gas/pos-inlet", + "/dnv-v2/vis-3-4a/621.21/S90/sec/411.1/C101/meta/qty-mass/cnt-fuel.oil/pos-inlet", + ] + + parser = LocalIdBuilderParsing() + for local_id_str in mqtt_examples: + try: + parsed = parser.parse(local_id_str) + standard = parsed.build() + mqtt_local_id = MqttLocalId(parsed) + print(f" Standard : {standard}") + print(f" MQTT : {mqtt_local_id}") + print() + except Exception: + pass + + # All 8 slots filled + full_primary = GmodPath.parse("411.1/C101.31-2", arg=version) + full_secondary = GmodPath.parse("411.1/C101.31-5", arg=version) + full_builder = ( + LocalIdBuilder.create(version) + .with_primary_item(full_primary) + .with_secondary_item(full_secondary) + .with_metadata_tag(codebooks.create_tag(CodebookName.Quantity, "temperature")) + .with_metadata_tag(codebooks.create_tag(CodebookName.Content, "exhaust.gas")) + .with_metadata_tag(codebooks.create_tag(CodebookName.Calculation, "average")) + .with_metadata_tag(codebooks.create_tag(CodebookName.State, "high")) + .with_metadata_tag(codebooks.create_tag(CodebookName.Command, "start")) + .with_metadata_tag(codebooks.create_tag(CodebookName.Type, "instantaneous")) + .with_metadata_tag(codebooks.create_tag(CodebookName.Position, "inlet")) + .with_metadata_tag(codebooks.create_tag(CodebookName.Detail, "my_sensor_42")) + ) + + full_standard = full_builder.build() + full_mqtt = MqttLocalId(full_builder) + + print(" All 8 slots + secondary + free-form detail:") + print(f" Standard : {full_standard}") + print(f" MQTT : {full_mqtt}") + print(" Slots : qty/cnt/calc/state/cmd/type/pos/detail") + print(f" Detail : '{full_standard.detail.value}' (is_custom: {full_standard.detail.is_custom})") # type: ignore + print() + + # Reading components + comp_primary = GmodPath.parse("411.1/C101.31-2", arg=version) + comp_secondary = GmodPath.parse("411.1/C101.31-5", arg=version) + comp_builder = ( + LocalIdBuilder.create(version) + .with_primary_item(comp_primary) + .with_secondary_item(comp_secondary) + .with_metadata_tag(codebooks.create_tag(CodebookName.Quantity, "temperature")) + .with_metadata_tag(codebooks.create_tag(CodebookName.Content, "exhaust.gas")) + ) + + mqtt_comp = MqttLocalId(comp_builder) + + print(" Reading components:") + print(f" vis_version : {mqtt_comp.vis_version}") + print(f" primary_item : {mqtt_comp.primary_item}") + print(f" secondary_item : {mqtt_comp.secondary_item}") + print(f" quantity : {mqtt_comp.quantity}") + print(f" content : {mqtt_comp.content}") + print(f" calculation : {mqtt_comp.calculation or '(none)'}") + print() + + # 8. MQTT LocalId: builder-level state and equality + print("8. MQTT LocalId: builder-level state and equality...") + + # Builder-level state not reflected in MQTT format + verbose_primary = GmodPath.parse("411.1/C101.63/S206", arg=version) + verbose_builder = ( + LocalIdBuilder.create(version) + .with_verbose_mode(True) + .with_primary_item(verbose_primary) + .with_metadata_tag(codebooks.create_tag(CodebookName.Quantity, "temperature")) + ) + + mqtt_verbose = MqttLocalId(verbose_builder) + + print(" Builder-level state (accessible via .builder):") + print(" verbose_mode/has_custom_tag/metadata_tags are not duplicated on MqttLocalId.") + print(" verbose_mode has no effect on the MQTT format (unlike LocalId.__str__()).") + print(f" builder.verbose_mode : {mqtt_verbose.builder.verbose_mode}") + print(f" builder.has_custom_tag : {mqtt_verbose.builder.has_custom_tag}") + print(f" builder.metadata_tags : {len(mqtt_verbose.builder.metadata_tags)} tag(s)") + print(f" MQTT (no '~' despite verbose_mode=True): {mqtt_verbose}") + print() + + # Equality + eq_primary = GmodPath.parse("411.1/C101.31-2", arg=version) + eq_builder = ( + LocalIdBuilder.create(version) + .with_primary_item(eq_primary) + .with_metadata_tag(codebooks.create_tag(CodebookName.Quantity, "temperature")) + ) + + a = MqttLocalId(eq_builder) + b = MqttLocalId(eq_builder) + c = MqttLocalId( + eq_builder.with_metadata_tag(codebooks.create_tag(CodebookName.Content, "exhaust.gas")) + ) + + print(" Equality:") + print(f" a == b (same builder) : {a == b}") + print(f" a == c (extra tag) : {a == c}") + print() + + print(" MQTT format differences vs standard:") + print(" - No leading '/'") + print(" - Underscores instead of slashes in paths") + print(" - No 'meta/' section") + print(" - '_' placeholder for absent metadata slots") + print(" - 8 fixed slots: qty/cnt/calc/state/cmd/type/pos/detail") + print("\n=== Advanced operations completed! ===")