diff --git a/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Modify.cs b/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Modify.cs index 6b34cde..31c06b9 100644 --- a/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Modify.cs +++ b/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Modify.cs @@ -9,7 +9,7 @@ public void SetCommand_Create_NodeIdDestinationsOnly() MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.Create( 1, new byte[] { 2, 3 }, - Array.Empty()); + Array.Empty()); Assert.AreEqual(CommandClassId.MultiChannelAssociation, MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.CommandClassId); Assert.AreEqual((byte)MultiChannelAssociationCommand.Set, MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.CommandId); @@ -23,13 +23,13 @@ public void SetCommand_Create_NodeIdDestinationsOnly() } [TestMethod] - public void SetCommand_Create_EndPointDestinationsOnly() + public void SetCommand_Create_EndpointDestinationsOnly() { MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand command = MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.Create( 2, Array.Empty(), - new EndPointDestination[] { new EndPointDestination(5, 1) }); + new EndpointDestination[] { new EndpointDestination(5, 1) }); // CC + Cmd + GroupId + Marker + MCNodeID + Properties = 6 bytes Assert.AreEqual(6, command.Frame.Data.Length); @@ -37,7 +37,7 @@ public void SetCommand_Create_EndPointDestinationsOnly() Assert.AreEqual((byte)2, parameters[0]); // GroupId Assert.AreEqual((byte)0x00, parameters[1]); // Marker Assert.AreEqual((byte)5, parameters[2]); // MCNodeID - Assert.AreEqual((byte)0x01, parameters[3]); // IsBitAddress=0|EP=1 + Assert.AreEqual((byte)0x01, parameters[3]); // EP=1 } [TestMethod] @@ -47,7 +47,7 @@ public void SetCommand_Create_MixedDestinations() MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.Create( 1, new byte[] { 2 }, - new EndPointDestination[] { new EndPointDestination(3, 1) }); + new EndpointDestination[] { new EndpointDestination(3, 1) }); // CC + Cmd + GroupId + NodeID(2) + Marker + MCNodeID(3) + Properties = 7 bytes Assert.AreEqual(7, command.Frame.Data.Length); @@ -56,23 +56,69 @@ public void SetCommand_Create_MixedDestinations() Assert.AreEqual((byte)2, parameters[1]); // NodeID Assert.AreEqual((byte)0x00, parameters[2]); // Marker Assert.AreEqual((byte)3, parameters[3]); // MCNodeID - Assert.AreEqual((byte)0x01, parameters[4]); // IsBitAddress=0|EP=1 + Assert.AreEqual((byte)0x01, parameters[4]); // EP=1 } [TestMethod] - public void SetCommand_Create_BitAddressEndPoint() + public void SetCommand_Create_BitAddressEndpoint() { + // Multiple endpoints 1,2,3 on node 4 → uses bit addressing (0b1000_0111 = 0x87) MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand command = MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.Create( 1, Array.Empty(), - new EndPointDestination[] { new EndPointDestination(4, new byte[] { 1, 2, 3 }) }); + new EndpointDestination[] { new EndpointDestination(4, new byte[] { 1, 2, 3 }) }); + // CC + Cmd + GroupId + Marker + MCNodeID + 0x87 = 6 bytes + Assert.AreEqual(6, command.Frame.Data.Length); + ReadOnlySpan parameters = command.Frame.CommandParameters.Span; + Assert.AreEqual((byte)1, parameters[0]); // GroupId + Assert.AreEqual((byte)0x00, parameters[1]); // Marker + Assert.AreEqual((byte)4, parameters[2]); // MCNodeID + Assert.AreEqual((byte)0x87, parameters[3]); // 0b1000_0111 = bit address with EP 1,2,3 + } + + [TestMethod] + public void SetCommand_Create_FallbackToIndividual_OnlyOneBitAddressable() + { + // Endpoints 0 and 1: only EP 1 is bit-addressable (1-7), EP 0 is not. + // Only 1 bit-addressable → no bit addressing, both written individually. + MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand command = + MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.Create( + 1, + Array.Empty(), + new EndpointDestination[] { new EndpointDestination(4, new byte[] { 0, 1 }) }); + + // CC + Cmd + GroupId + Marker + (NodeId+EP0) + (NodeId+EP1) = 8 bytes + Assert.AreEqual(8, command.Frame.Data.Length); ReadOnlySpan parameters = command.Frame.CommandParameters.Span; Assert.AreEqual((byte)1, parameters[0]); // GroupId Assert.AreEqual((byte)0x00, parameters[1]); // Marker Assert.AreEqual((byte)4, parameters[2]); // MCNodeID - Assert.AreEqual((byte)0x87, parameters[3]); // IsBitAddress=1|EP=0x07 → 0x80 | 0x07 = 0x87 + Assert.AreEqual((byte)0x00, parameters[3]); // EP=0 + Assert.AreEqual((byte)4, parameters[4]); // MCNodeID + Assert.AreEqual((byte)0x01, parameters[5]); // EP=1 + } + + [TestMethod] + public void SetCommand_Create_MixedBitAddressAndIndividual() + { + // Endpoints 0,1,2,3: EP 0 is not bit-addressable, EPs 1,2,3 are → bit-addressed + EP 0 individual + MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand command = + MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.Create( + 1, + Array.Empty(), + new EndpointDestination[] { new EndpointDestination(4, new byte[] { 0, 1, 2, 3 }) }); + + // CC + Cmd + GroupId + Marker + (NodeId+0x87 bit-addressed) + (NodeId+EP0) = 8 bytes + Assert.AreEqual(8, command.Frame.Data.Length); + ReadOnlySpan parameters = command.Frame.CommandParameters.Span; + Assert.AreEqual((byte)1, parameters[0]); // GroupId + Assert.AreEqual((byte)0x00, parameters[1]); // Marker + Assert.AreEqual((byte)4, parameters[2]); // MCNodeID (bit-addressed entry) + Assert.AreEqual((byte)0x87, parameters[3]); // 0b1000_0111 = bit address with EP 1,2,3 + Assert.AreEqual((byte)4, parameters[4]); // MCNodeID (individual entry) + Assert.AreEqual((byte)0x00, parameters[5]); // EP=0 } [TestMethod] @@ -82,7 +128,7 @@ public void SetCommand_Create_NoDestinations() MultiChannelAssociationCommandClass.MultiChannelAssociationSetCommand.Create( 1, Array.Empty(), - Array.Empty()); + Array.Empty()); // CC + Cmd + GroupId = 3 bytes. No marker because no EP destinations. Assert.AreEqual(3, command.Frame.Data.Length); @@ -97,7 +143,7 @@ public void RemoveCommand_Create_SpecificNodeIdFromGroup() MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.Create( 3, new byte[] { 5 }, - Array.Empty()); + Array.Empty()); Assert.AreEqual(CommandClassId.MultiChannelAssociation, MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.CommandClassId); Assert.AreEqual((byte)MultiChannelAssociationCommand.Remove, MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.CommandId); @@ -115,7 +161,7 @@ public void RemoveCommand_Create_AllFromGroup() MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.Create( 3, Array.Empty(), - Array.Empty()); + Array.Empty()); // CC + Cmd + GroupId = 3 bytes Assert.AreEqual(3, command.Frame.Data.Length); @@ -131,7 +177,7 @@ public void RemoveCommand_Create_AllFromAllGroups() MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.Create( 0, Array.Empty(), - Array.Empty()); + Array.Empty()); Assert.AreEqual(3, command.Frame.Data.Length); ReadOnlySpan parameters = command.Frame.CommandParameters.Span; @@ -146,7 +192,7 @@ public void RemoveCommand_Create_SpecificNodeIdFromAllGroups() MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.Create( 0, new byte[] { 7 }, - Array.Empty()); + Array.Empty()); ReadOnlySpan parameters = command.Frame.CommandParameters.Span; Assert.AreEqual((byte)0, parameters[0]); // GroupId = 0 @@ -154,19 +200,19 @@ public void RemoveCommand_Create_SpecificNodeIdFromAllGroups() } [TestMethod] - public void RemoveCommand_Create_EndPointFromGroup() + public void RemoveCommand_Create_EndpointFromGroup() { MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand command = MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.Create( 3, Array.Empty(), - new EndPointDestination[] { new EndPointDestination(5, 2) }); + new EndpointDestination[] { new EndpointDestination(5, 2) }); ReadOnlySpan parameters = command.Frame.CommandParameters.Span; Assert.AreEqual((byte)3, parameters[0]); // GroupId Assert.AreEqual((byte)0x00, parameters[1]); // Marker Assert.AreEqual((byte)5, parameters[2]); // MCNodeID - Assert.AreEqual((byte)0x02, parameters[3]); // IsBitAddress=0|EP=2 + Assert.AreEqual((byte)0x02, parameters[3]); // EP=2 } [TestMethod] @@ -176,13 +222,13 @@ public void RemoveCommand_Create_MixedFromGroup() MultiChannelAssociationCommandClass.MultiChannelAssociationRemoveCommand.Create( 3, new byte[] { 2 }, - new EndPointDestination[] { new EndPointDestination(5, 1) }); + new EndpointDestination[] { new EndpointDestination(5, 1) }); ReadOnlySpan parameters = command.Frame.CommandParameters.Span; Assert.AreEqual((byte)3, parameters[0]); // GroupId Assert.AreEqual((byte)2, parameters[1]); // NodeID Assert.AreEqual((byte)0x00, parameters[2]); // Marker Assert.AreEqual((byte)5, parameters[3]); // MCNodeID - Assert.AreEqual((byte)0x01, parameters[4]); // IsBitAddress=0|EP=1 + Assert.AreEqual((byte)0x01, parameters[4]); // EP=1 } } diff --git a/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Report.cs b/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Report.cs index 9221ec6..632d60b 100644 --- a/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Report.cs +++ b/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.Report.cs @@ -17,104 +17,116 @@ public void GetCommand_Create_HasCorrectFormat() } [TestMethod] - public void Report_Parse_NodeIdDestinationsOnly() + public void Report_ParseInto_NodeIdDestinationsOnly() { // CC=0x8E, Cmd=0x03, GroupId=1, MaxNodes=5, ReportsToFollow=0, NodeID=2, NodeID=3 byte[] data = [0x8E, 0x03, 0x01, 0x05, 0x00, 0x02, 0x03]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); - - Assert.AreEqual((byte)1, report.GroupingIdentifier); - Assert.AreEqual((byte)5, report.MaxNodesSupported); - Assert.AreEqual((byte)0, report.ReportsToFollow); - Assert.HasCount(2, report.NodeIdDestinations); - Assert.AreEqual((byte)2, report.NodeIdDestinations[0]); - Assert.AreEqual((byte)3, report.NodeIdDestinations[1]); - Assert.IsEmpty(report.EndPointDestinations); + List nodeIdDestinations = []; + List endpointDestinations = []; + (byte maxNodesSupported, byte reportsToFollow) = + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); + + Assert.AreEqual((byte)5, maxNodesSupported); + Assert.AreEqual((byte)0, reportsToFollow); + Assert.HasCount(2, nodeIdDestinations); + Assert.AreEqual((byte)2, nodeIdDestinations[0]); + Assert.AreEqual((byte)3, nodeIdDestinations[1]); + Assert.IsEmpty(endpointDestinations); } [TestMethod] - public void Report_Parse_EndPointDestinationsOnly() + public void Report_ParseInto_EndpointDestinationsOnly_GroupedByNodeId() { // CC=0x8E, Cmd=0x03, GroupId=2, MaxNodes=10, ReportsToFollow=0, - // Marker=0x00, MCNodeID=5, BitAddr=0|EP=1, MCNodeID=5, BitAddr=0|EP=2 + // Marker=0x00, MCNodeID=5, EP=1, MCNodeID=5, EP=2 byte[] data = [0x8E, 0x03, 0x02, 0x0A, 0x00, 0x00, 0x05, 0x01, 0x05, 0x02]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); - - Assert.AreEqual((byte)2, report.GroupingIdentifier); - Assert.AreEqual((byte)10, report.MaxNodesSupported); - Assert.IsEmpty(report.NodeIdDestinations); - Assert.HasCount(2, report.EndPointDestinations); - Assert.AreEqual((byte)5, report.EndPointDestinations[0].NodeId); - Assert.IsFalse(report.EndPointDestinations[0].IsBitAddress); - Assert.AreEqual((byte)1, report.EndPointDestinations[0].Destination); - Assert.AreEqual((byte)5, report.EndPointDestinations[1].NodeId); - Assert.IsFalse(report.EndPointDestinations[1].IsBitAddress); - Assert.AreEqual((byte)2, report.EndPointDestinations[1].Destination); + List nodeIdDestinations = []; + List endpointDestinations = []; + (byte maxNodesSupported, byte reportsToFollow) = + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); + + Assert.AreEqual((byte)10, maxNodesSupported); + Assert.AreEqual((byte)0, reportsToFollow); + Assert.IsEmpty(nodeIdDestinations); + // Two wire entries for node 5, EP1 and EP2 → grouped into one EndpointDestination + Assert.HasCount(1, endpointDestinations); + Assert.AreEqual((byte)5, endpointDestinations[0].NodeId); + Assert.HasCount(2, endpointDestinations[0].Endpoints); + Assert.AreEqual((byte)1, endpointDestinations[0].Endpoints[0]); + Assert.AreEqual((byte)2, endpointDestinations[0].Endpoints[1]); } [TestMethod] - public void Report_Parse_MixedDestinations() + public void Report_ParseInto_MixedDestinations() { // CC=0x8E, Cmd=0x03, GroupId=1, MaxNodes=10, ReportsToFollow=0, - // NodeID=1, NodeID=2, Marker=0x00, MCNodeID=3, BitAddr=0|EP=1 + // NodeID=1, NodeID=2, Marker=0x00, MCNodeID=3, EP=1 byte[] data = [0x8E, 0x03, 0x01, 0x0A, 0x00, 0x01, 0x02, 0x00, 0x03, 0x01]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); - - Assert.AreEqual((byte)1, report.GroupingIdentifier); - Assert.HasCount(2, report.NodeIdDestinations); - Assert.AreEqual((byte)1, report.NodeIdDestinations[0]); - Assert.AreEqual((byte)2, report.NodeIdDestinations[1]); - Assert.HasCount(1, report.EndPointDestinations); - Assert.AreEqual((byte)3, report.EndPointDestinations[0].NodeId); - Assert.IsFalse(report.EndPointDestinations[0].IsBitAddress); - Assert.AreEqual((byte)1, report.EndPointDestinations[0].Destination); + List nodeIdDestinations = []; + List endpointDestinations = []; + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); + + Assert.HasCount(2, nodeIdDestinations); + Assert.AreEqual((byte)1, nodeIdDestinations[0]); + Assert.AreEqual((byte)2, nodeIdDestinations[1]); + Assert.HasCount(1, endpointDestinations); + Assert.AreEqual((byte)3, endpointDestinations[0].NodeId); + Assert.HasCount(1, endpointDestinations[0].Endpoints); + Assert.AreEqual((byte)1, endpointDestinations[0].Endpoints[0]); } [TestMethod] - public void Report_Parse_EmptyDestinations() + public void Report_ParseInto_EmptyDestinations() { // CC=0x8E, Cmd=0x03, GroupId=1, MaxNodes=5, ReportsToFollow=0, no destinations byte[] data = [0x8E, 0x03, 0x01, 0x05, 0x00]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); + List nodeIdDestinations = []; + List endpointDestinations = []; + (byte maxNodesSupported, byte reportsToFollow) = + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); - Assert.AreEqual((byte)1, report.GroupingIdentifier); - Assert.AreEqual((byte)5, report.MaxNodesSupported); - Assert.AreEqual((byte)0, report.ReportsToFollow); - Assert.IsEmpty(report.NodeIdDestinations); - Assert.IsEmpty(report.EndPointDestinations); + Assert.AreEqual((byte)5, maxNodesSupported); + Assert.AreEqual((byte)0, reportsToFollow); + Assert.IsEmpty(nodeIdDestinations); + Assert.IsEmpty(endpointDestinations); } [TestMethod] - public void Report_Parse_BitAddressFlag() + public void Report_ParseInto_BitAddressFlag_ExpandsEndpoints() { // CC=0x8E, Cmd=0x03, GroupId=1, MaxNodes=10, ReportsToFollow=0, - // Marker=0x00, MCNodeID=4, BitAddr=1|EP=0x07 → properties byte = 0x87 + // Marker=0x00, MCNodeID=4, BitAddr=1|0b0000111 → properties byte = 0x87 byte[] data = [0x8E, 0x03, 0x01, 0x0A, 0x00, 0x00, 0x04, 0x87]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); - - Assert.HasCount(1, report.EndPointDestinations); - Assert.AreEqual((byte)4, report.EndPointDestinations[0].NodeId); - Assert.IsTrue(report.EndPointDestinations[0].IsBitAddress); - Assert.AreEqual((byte)0x07, report.EndPointDestinations[0].Destination); + List nodeIdDestinations = []; + List endpointDestinations = []; + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); + + // 0x87 = 0b1000_0111 → bit address, endpoints 1, 2, 3 + Assert.HasCount(1, endpointDestinations); + Assert.AreEqual((byte)4, endpointDestinations[0].NodeId); + Assert.HasCount(3, endpointDestinations[0].Endpoints); + Assert.AreEqual((byte)1, endpointDestinations[0].Endpoints[0]); + Assert.AreEqual((byte)2, endpointDestinations[0].Endpoints[1]); + Assert.AreEqual((byte)3, endpointDestinations[0].Endpoints[2]); } [TestMethod] - public void Report_Parse_EndPointZero_V3() + public void Report_ParseInto_EndpointZero_V3() { // V3 allows EndPoint 0 (Root Device destination). // CC=0x8E, Cmd=0x03, GroupId=1, MaxNodes=5, ReportsToFollow=0, @@ -122,42 +134,50 @@ public void Report_Parse_EndPointZero_V3() byte[] data = [0x8E, 0x03, 0x01, 0x05, 0x00, 0x00, 0x01, 0x00]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); + List nodeIdDestinations = []; + List endpointDestinations = []; + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); - Assert.HasCount(1, report.EndPointDestinations); - Assert.AreEqual((byte)1, report.EndPointDestinations[0].NodeId); - Assert.IsFalse(report.EndPointDestinations[0].IsBitAddress); - Assert.AreEqual((byte)0, report.EndPointDestinations[0].Destination); + Assert.HasCount(1, endpointDestinations); + Assert.AreEqual((byte)1, endpointDestinations[0].NodeId); + Assert.HasCount(1, endpointDestinations[0].Endpoints); + Assert.AreEqual((byte)0, endpointDestinations[0].Endpoints[0]); } [TestMethod] - public void Report_Parse_ReportsToFollow() + public void Report_ParseInto_ReportsToFollow() { // CC=0x8E, Cmd=0x03, GroupId=1, MaxNodes=20, ReportsToFollow=2, NodeID=1 byte[] data = [0x8E, 0x03, 0x01, 0x14, 0x02, 0x01]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); + List nodeIdDestinations = []; + List endpointDestinations = []; + (byte maxNodesSupported, byte reportsToFollow) = + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); - Assert.AreEqual((byte)2, report.ReportsToFollow); - Assert.HasCount(1, report.NodeIdDestinations); + Assert.AreEqual((byte)2, reportsToFollow); + Assert.HasCount(1, nodeIdDestinations); } [TestMethod] - public void Report_Parse_TooShort_Throws() + public void Report_ParseInto_TooShort_Throws() { // Only 2 parameter bytes, need at least 3 (GroupId + MaxNodes + ReportsToFollow) byte[] data = [0x8E, 0x03, 0x01, 0x05]; CommandClassFrame frame = new(data); + List nodeIdDestinations = []; + List endpointDestinations = []; Assert.Throws( - () => MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance)); + () => MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance)); } [TestMethod] - public void Report_Parse_MultipleEndPointsSameNode() + public void Report_ParseInto_MultipleEndpointsSameNode_GroupedByNodeId() { // Same NodeID with different endpoints (e.g. power strip with 3 outlets). // CC=0x8E, Cmd=0x03, GroupId=1, MaxNodes=10, ReportsToFollow=0, @@ -165,16 +185,55 @@ public void Report_Parse_MultipleEndPointsSameNode() byte[] data = [0x8E, 0x03, 0x01, 0x0A, 0x00, 0x00, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03]; CommandClassFrame frame = new(data); - MultiChannelAssociationReport report = - MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.Parse(frame, NullLogger.Instance); - - Assert.HasCount(3, report.EndPointDestinations); - Assert.AreEqual((byte)1, report.EndPointDestinations[0].Destination); - Assert.AreEqual((byte)2, report.EndPointDestinations[1].Destination); - Assert.AreEqual((byte)3, report.EndPointDestinations[2].Destination); - for (int i = 0; i < 3; i++) - { - Assert.AreEqual((byte)5, report.EndPointDestinations[i].NodeId); - } + List nodeIdDestinations = []; + List endpointDestinations = []; + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame, nodeIdDestinations, endpointDestinations, NullLogger.Instance); + + // Three wire entries for node 5 → grouped into one EndpointDestination + Assert.HasCount(1, endpointDestinations); + Assert.AreEqual((byte)5, endpointDestinations[0].NodeId); + Assert.HasCount(3, endpointDestinations[0].Endpoints); + Assert.AreEqual((byte)1, endpointDestinations[0].Endpoints[0]); + Assert.AreEqual((byte)2, endpointDestinations[0].Endpoints[1]); + Assert.AreEqual((byte)3, endpointDestinations[0].Endpoints[2]); + } + + [TestMethod] + public void Report_ParseInto_MultiFrameAggregation() + { + // Frame 1: GroupId=1, MaxNodes=20, ReportsToFollow=1, NodeID=1, NodeID=2 + byte[] data1 = [0x8E, 0x03, 0x01, 0x14, 0x01, 0x01, 0x02]; + CommandClassFrame frame1 = new(data1); + + // Frame 2: GroupId=1, MaxNodes=20, ReportsToFollow=0, Marker=0x00, MCNodeID=5 EP=1 + byte[] data2 = [0x8E, 0x03, 0x01, 0x14, 0x00, 0x00, 0x05, 0x01]; + CommandClassFrame frame2 = new(data2); + + List allNodeIdDestinations = []; + List allEndpointDestinations = []; + + (byte maxNodesSupported1, byte reportsToFollow1) = + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame1, allNodeIdDestinations, allEndpointDestinations, NullLogger.Instance); + + Assert.AreEqual((byte)20, maxNodesSupported1); + Assert.AreEqual((byte)1, reportsToFollow1); + + (byte maxNodesSupported2, byte reportsToFollow2) = + MultiChannelAssociationCommandClass.MultiChannelAssociationReportCommand.ParseInto( + frame2, allNodeIdDestinations, allEndpointDestinations, NullLogger.Instance); + + Assert.AreEqual((byte)20, maxNodesSupported2); + Assert.AreEqual((byte)0, reportsToFollow2); + + // Combined result: 2 NodeID destinations + 1 endpoint destination + Assert.HasCount(2, allNodeIdDestinations); + Assert.AreEqual((byte)1, allNodeIdDestinations[0]); + Assert.AreEqual((byte)2, allNodeIdDestinations[1]); + Assert.HasCount(1, allEndpointDestinations); + Assert.AreEqual((byte)5, allEndpointDestinations[0].NodeId); + Assert.HasCount(1, allEndpointDestinations[0].Endpoints); + Assert.AreEqual((byte)1, allEndpointDestinations[0].Endpoints[0]); } } diff --git a/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.cs b/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.cs index 41cb4d4..d93d718 100644 --- a/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.cs +++ b/src/ZWave.CommandClasses.Tests/MultiChannelAssociationCommandClassTests.cs @@ -4,67 +4,62 @@ namespace ZWave.CommandClasses.Tests; public partial class MultiChannelAssociationCommandClassTests { [TestMethod] - public void EndPointDestination_SingleEndpoint_HasCorrectProperties() + public void EndpointDestination_SingleEndpoint_HasCorrectProperties() { - EndPointDestination dest = new EndPointDestination(5, 3); + EndpointDestination dest = new EndpointDestination(5, 3); Assert.AreEqual((byte)5, dest.NodeId); - Assert.IsFalse(dest.IsBitAddress); - Assert.AreEqual((byte)3, dest.Destination); + Assert.HasCount(1, dest.Endpoints); + Assert.AreEqual((byte)3, dest.Endpoints[0]); } [TestMethod] - public void EndPointDestination_SingleEndpoint_Zero_RootDevice() + public void EndpointDestination_SingleEndpoint_Zero_RootDevice() { - EndPointDestination dest = new EndPointDestination(1, 0); + EndpointDestination dest = new EndpointDestination(1, 0); Assert.AreEqual((byte)1, dest.NodeId); - Assert.IsFalse(dest.IsBitAddress); - Assert.AreEqual((byte)0, dest.Destination); + Assert.HasCount(1, dest.Endpoints); + Assert.AreEqual((byte)0, dest.Endpoints[0]); } [TestMethod] - public void EndPointDestination_MultipleEndpoints_HasCorrectProperties() + public void EndpointDestination_MultipleEndpoints_HasCorrectProperties() { - EndPointDestination dest = new EndPointDestination(4, new byte[] { 1, 2, 3 }); + EndpointDestination dest = new EndpointDestination(4, new byte[] { 1, 2, 3 }); Assert.AreEqual((byte)4, dest.NodeId); - Assert.IsTrue(dest.IsBitAddress); - // Endpoints 1, 2, 3 → bits 0, 1, 2 → 0b00000111 = 0x07 - Assert.AreEqual((byte)0x07, dest.Destination); + Assert.HasCount(3, dest.Endpoints); + Assert.AreEqual((byte)1, dest.Endpoints[0]); + Assert.AreEqual((byte)2, dest.Endpoints[1]); + Assert.AreEqual((byte)3, dest.Endpoints[2]); } [TestMethod] - public void EndPointDestination_MultipleEndpoints_SingleEndpoint() + public void EndpointDestination_MultipleEndpoints_SingleItem() { - EndPointDestination dest = new EndPointDestination(4, new byte[] { 5 }); + EndpointDestination dest = new EndpointDestination(4, new byte[] { 5 }); - Assert.IsTrue(dest.IsBitAddress); - // Endpoint 5 → bit 4 → 0b00010000 = 0x10 - Assert.AreEqual((byte)0x10, dest.Destination); + Assert.HasCount(1, dest.Endpoints); + Assert.AreEqual((byte)5, dest.Endpoints[0]); } [TestMethod] - public void EndPointDestination_MultipleEndpoints_AllEndpoints() + public void EndpointDestination_MultipleEndpoints_AllEndpoints() { - EndPointDestination dest = new EndPointDestination(4, new byte[] { 1, 2, 3, 4, 5, 6, 7 }); + EndpointDestination dest = new EndpointDestination(4, new byte[] { 1, 2, 3, 4, 5, 6, 7 }); - Assert.IsTrue(dest.IsBitAddress); - // Endpoints 1-7 → bits 0-6 → 0b01111111 = 0x7F - Assert.AreEqual((byte)0x7F, dest.Destination); + Assert.HasCount(7, dest.Endpoints); + for (int i = 0; i < 7; i++) + { + Assert.AreEqual((byte)(i + 1), dest.Endpoints[i]); + } } [TestMethod] - public void EndPointDestination_MultipleEndpoints_EndpointZero_Throws() + public void EndpointDestination_MultipleEndpoints_Empty_Throws() { - Assert.Throws( - () => new EndPointDestination(4, new byte[] { 0 })); - } - - [TestMethod] - public void EndPointDestination_MultipleEndpoints_EndpointTooHigh_Throws() - { - Assert.Throws( - () => new EndPointDestination(4, new byte[] { 8 })); + Assert.Throws( + () => new EndpointDestination(4, (ReadOnlySpan)Array.Empty())); } } diff --git a/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Modify.cs b/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Modify.cs index 1bff15b..941789e 100644 --- a/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Modify.cs +++ b/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Modify.cs @@ -7,18 +7,18 @@ public sealed partial class MultiChannelAssociationCommandClass /// /// The association group identifier (1–255). /// NodeID-only destinations to add. - /// End Point destinations to add. + /// End Point destinations to add. /// A cancellation token. public async Task SetAsync( byte groupingIdentifier, IReadOnlyList nodeIdDestinations, - IReadOnlyList endPointDestinations, + IReadOnlyList endpointDestinations, CancellationToken cancellationToken) { MultiChannelAssociationSetCommand command = MultiChannelAssociationSetCommand.Create( groupingIdentifier, nodeIdDestinations, - endPointDestinations); + endpointDestinations); await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); } @@ -28,7 +28,7 @@ public async Task SetAsync( /// /// /// Per the spec, the combination of , - /// , and + /// , and /// determines the behavior: /// /// @@ -40,32 +40,54 @@ public async Task SetAsync( /// /// The association group identifier, or 0 to target all groups. /// NodeID-only destinations to remove. - /// End Point destinations to remove. + /// End Point destinations to remove. /// A cancellation token. public async Task RemoveAsync( byte groupingIdentifier, IReadOnlyList nodeIdDestinations, - IReadOnlyList endPointDestinations, + IReadOnlyList endpointDestinations, CancellationToken cancellationToken) { MultiChannelAssociationRemoveCommand command = MultiChannelAssociationRemoveCommand.Create( groupingIdentifier, nodeIdDestinations, - endPointDestinations); + endpointDestinations); await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); } + private static int CountWireEntries(EndpointDestination dest) + { + int bitAddressable = 0; + int other = 0; + for (int i = 0; i < dest.Endpoints.Count; i++) + { + byte ep = dest.Endpoints[i]; + if (ep >= 1 && ep <= 7) + { + bitAddressable++; + } + else + { + other++; + } + } + return (bitAddressable >= 2 ? 1 : bitAddressable) + other; + } + private static int ComputeDestinationPayloadLength( IReadOnlyList nodeIdDestinations, - IReadOnlyList endPointDestinations) + IReadOnlyList endpointDestinations) { int length = nodeIdDestinations.Count; - if (endPointDestinations.Count > 0) + int wireEntryCount = 0; + for (int i = 0; i < endpointDestinations.Count; i++) { - // Marker byte + 2 bytes per End Point destination (NodeID + BitAddress|EndPoint). - length += 1 + (endPointDestinations.Count * 2); + wireEntryCount += CountWireEntries(endpointDestinations[i]); + } + if (wireEntryCount > 0) + { + length += 1 + (wireEntryCount * 2); } - return length; } @@ -73,25 +95,59 @@ private static void WriteDestinationPayload( Span buffer, int offset, IReadOnlyList nodeIdDestinations, - IReadOnlyList endPointDestinations) + IReadOnlyList endpointDestinations) { - // Write NodeID destinations. for (int i = 0; i < nodeIdDestinations.Count; i++) { buffer[offset++] = nodeIdDestinations[i]; } - if (endPointDestinations.Count > 0) + bool hasEndpoints = false; + for (int i = 0; i < endpointDestinations.Count; i++) + { + if (endpointDestinations[i].Endpoints.Count > 0) + { + hasEndpoints = true; + break; + } + } + + if (hasEndpoints) { - // Write marker. buffer[offset++] = Marker; - // Write End Point destinations. - for (int i = 0; i < endPointDestinations.Count; i++) + for (int i = 0; i < endpointDestinations.Count; i++) { - EndPointDestination dest = endPointDestinations[i]; - buffer[offset++] = dest.NodeId; - buffer[offset++] = (byte)((dest.IsBitAddress ? 0x80 : 0x00) | (dest.Destination & 0x7F)); + EndpointDestination dest = endpointDestinations[i]; + + byte bitMask = 0; + int bitAddressableCount = 0; + for (int j = 0; j < dest.Endpoints.Count; j++) + { + byte ep = dest.Endpoints[j]; + if (ep >= 1 && ep <= 7) + { + bitMask |= (byte)(1 << (ep - 1)); + bitAddressableCount++; + } + } + + bool useBitAddress = bitAddressableCount >= 2; + + if (useBitAddress) + { + buffer[offset++] = dest.NodeId; + buffer[offset++] = (byte)(0b1000_0000 | bitMask); + } + + for (int j = 0; j < dest.Endpoints.Count; j++) + { + byte ep = dest.Endpoints[j]; + if (useBitAddress && ep >= 1 && ep <= 7) + continue; + buffer[offset++] = dest.NodeId; + buffer[offset++] = (byte)(ep & 0b0111_1111); + } } } } @@ -112,12 +168,12 @@ public MultiChannelAssociationSetCommand(CommandClassFrame frame) public static MultiChannelAssociationSetCommand Create( byte groupingIdentifier, IReadOnlyList nodeIdDestinations, - IReadOnlyList endPointDestinations) + IReadOnlyList endpointDestinations) { - int payloadLength = 1 + ComputeDestinationPayloadLength(nodeIdDestinations, endPointDestinations); + int payloadLength = 1 + ComputeDestinationPayloadLength(nodeIdDestinations, endpointDestinations); Span commandParameters = stackalloc byte[payloadLength]; commandParameters[0] = groupingIdentifier; - WriteDestinationPayload(commandParameters, 1, nodeIdDestinations, endPointDestinations); + WriteDestinationPayload(commandParameters, 1, nodeIdDestinations, endpointDestinations); CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); return new MultiChannelAssociationSetCommand(frame); @@ -140,12 +196,12 @@ public MultiChannelAssociationRemoveCommand(CommandClassFrame frame) public static MultiChannelAssociationRemoveCommand Create( byte groupingIdentifier, IReadOnlyList nodeIdDestinations, - IReadOnlyList endPointDestinations) + IReadOnlyList endpointDestinations) { - int payloadLength = 1 + ComputeDestinationPayloadLength(nodeIdDestinations, endPointDestinations); + int payloadLength = 1 + ComputeDestinationPayloadLength(nodeIdDestinations, endpointDestinations); Span commandParameters = stackalloc byte[payloadLength]; commandParameters[0] = groupingIdentifier; - WriteDestinationPayload(commandParameters, 1, nodeIdDestinations, endPointDestinations); + WriteDestinationPayload(commandParameters, 1, nodeIdDestinations, endpointDestinations); CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); return new MultiChannelAssociationRemoveCommand(frame); diff --git a/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Report.cs b/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Report.cs index 43c2eed..0ca4028 100644 --- a/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Report.cs +++ b/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.Report.cs @@ -17,11 +17,6 @@ public readonly record struct MultiChannelAssociationReport( /// byte MaxNodesSupported, - /// - /// The number of report frames that will follow this report. - /// - byte ReportsToFollow, - /// /// The NodeID-only destinations in this association group. /// @@ -30,7 +25,7 @@ public readonly record struct MultiChannelAssociationReport( /// /// The End Point destinations in this association group. /// - IReadOnlyList EndPointDestinations); + IReadOnlyList EndpointDestinations); public sealed partial class MultiChannelAssociationCommandClass { @@ -44,7 +39,7 @@ public sealed partial class MultiChannelAssociationCommandClass /// public IReadOnlyDictionary GroupReports => _groupReports; - private readonly Dictionary _groupReports = new Dictionary(); + private readonly Dictionary _groupReports = []; private void UpdateGroupReport(MultiChannelAssociationReport report) { @@ -58,10 +53,27 @@ public async Task GetAsync(byte groupingIdentifie { MultiChannelAssociationGetCommand command = MultiChannelAssociationGetCommand.Create(groupingIdentifier); await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); - CommandClassFrame reportFrame = await AwaitNextReportAsync( - frame => frame.CommandParameters.Length >= 1 && frame.CommandParameters.Span[0] == groupingIdentifier, - cancellationToken).ConfigureAwait(false); - MultiChannelAssociationReport report = MultiChannelAssociationReportCommand.Parse(reportFrame, Logger); + + List allNodeIdDestinations = []; + List allEndpointDestinations = []; + byte maxNodesSupported = 0; + + byte reportsToFollow; + do + { + CommandClassFrame reportFrame = await AwaitNextReportAsync( + frame => frame.CommandParameters.Length >= 1 && frame.CommandParameters.Span[0] == groupingIdentifier, + cancellationToken).ConfigureAwait(false); + (maxNodesSupported, reportsToFollow) = MultiChannelAssociationReportCommand.ParseInto( + reportFrame, allNodeIdDestinations, allEndpointDestinations, Logger); + } + while (reportsToFollow > 0); + + MultiChannelAssociationReport report = new( + groupingIdentifier, + maxNodesSupported, + allNodeIdDestinations, + allEndpointDestinations); UpdateGroupReport(report); OnReportReceived?.Invoke(report); return report; @@ -101,7 +113,11 @@ public MultiChannelAssociationReportCommand(CommandClassFrame frame) public CommandClassFrame Frame { get; } - public static MultiChannelAssociationReport Parse(CommandClassFrame frame, ILogger logger) + public static (byte MaxNodesSupported, byte ReportsToFollow) ParseInto( + CommandClassFrame frame, + List nodeIdDestinations, + List endpointDestinations, + ILogger logger) { if (frame.CommandParameters.Length < 3) { @@ -117,63 +133,74 @@ public static MultiChannelAssociationReport Parse(CommandClassFrame frame, ILogg byte groupingIdentifier = span[0]; byte maxNodesSupported = span[1]; byte reportsToFollow = span[2]; - ReadOnlySpan destinationData = span[3..]; - // Find the marker byte (0x00) to split NodeID destinations from End Point destinations. int markerIndex = destinationData.IndexOf(Marker); - List nodeIdDestinations; - List endPointDestinations; - if (markerIndex < 0) { - // No marker — all destinations are NodeID-only. - nodeIdDestinations = new List(destinationData.Length); for (int i = 0; i < destinationData.Length; i++) { nodeIdDestinations.Add(destinationData[i]); } - - endPointDestinations = new List(); } else { - // Parse NodeID destinations before the marker. - nodeIdDestinations = new List(markerIndex); for (int i = 0; i < markerIndex; i++) { nodeIdDestinations.Add(destinationData[i]); } - // Parse End Point destinations after the marker. - // Each End Point destination is 2 bytes: NodeID + (BitAddress | EndPoint). - ReadOnlySpan endPointData = destinationData[(markerIndex + 1)..]; - int endPointCount = endPointData.Length / 2; - endPointDestinations = new List(endPointCount); - for (int i = 0; i + 1 < endPointData.Length; i += 2) + // Parse endpoint destinations, group by NodeId, expand bit masks. + ReadOnlySpan endpointData = destinationData[(markerIndex + 1)..]; + Dictionary>? grouped = null; + for (int i = 0; i + 1 < endpointData.Length; i += 2) + { + byte nodeId = endpointData[i]; + byte properties = endpointData[i + 1]; + bool bitAddress = (properties & 0b1000_0000) != 0; + byte endpointValue = (byte)(properties & 0b0111_1111); + + grouped ??= []; + if (!grouped.TryGetValue(nodeId, out List? endpoints)) + { + endpoints = []; + grouped[nodeId] = endpoints; + } + + if (bitAddress) + { + for (int bit = 0; bit < 7; bit++) + { + if ((endpointValue & (1 << bit)) != 0) + { + endpoints.Add((byte)(bit + 1)); + } + } + } + else + { + endpoints.Add(endpointValue); + } + } + + if (grouped != null) { - byte nodeId = endPointData[i]; - byte properties = endPointData[i + 1]; - bool bitAddress = (properties & 0x80) != 0; - byte endPoint = (byte)(properties & 0x7F); - endPointDestinations.Add(new EndPointDestination(nodeId, bitAddress, endPoint)); + foreach (KeyValuePair> entry in grouped) + { + endpointDestinations.Add(new EndpointDestination(entry.Key, (ReadOnlySpan)entry.Value.ToArray())); + } } - if (endPointData.Length % 2 != 0) + if (endpointData.Length % 2 != 0) { logger.LogWarning( - "Multi Channel Association Report has a trailing byte after the marker (odd End Point data length: {Length})", - endPointData.Length); + "Multi Channel Association Report has a trailing byte after the marker (odd endpoint data length: {Length})", + endpointData.Length); } } - return new MultiChannelAssociationReport( - groupingIdentifier, - maxNodesSupported, - reportsToFollow, - nodeIdDestinations, - endPointDestinations); + return (maxNodesSupported, reportsToFollow); } } } diff --git a/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.cs b/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.cs index 3177700..2112b00 100644 --- a/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.cs +++ b/src/ZWave.CommandClasses/MultiChannelAssociationCommandClass.cs @@ -36,63 +36,31 @@ public enum MultiChannelAssociationCommand : byte } /// -/// Represents an End Point destination in a Multi Channel association. +/// Represents an endpoint destination in a Multi Channel association. /// -/// -/// -/// An End Point destination identifies a specific endpoint on a node. Use the constructor -/// for a single endpoint destination, or the -/// overload for a destination -/// destination targeting multiple endpoints simultaneously. -/// -/// -/// When is true, is a bit mask where -/// bit 0 = endpoint 1, bit 1 = endpoint 2, etc. (endpoints 1–7). -/// When false, is the single endpoint index (0–127). -/// -/// -public readonly record struct EndPointDestination +public readonly record struct EndpointDestination { /// - /// Creates an End Point destination targeting a single endpoint on a node. + /// Creates an endpoint destination targeting a single endpoint on a node. /// - /// The NodeID of the destination. - /// The endpoint index (0–127). - public EndPointDestination(byte nodeId, byte endPoint) + public EndpointDestination(byte nodeId, byte endpoint) { NodeId = nodeId; - IsBitAddress = false; - Destination = endPoint; - } - - internal EndPointDestination(byte nodeId, bool isBitAddress, byte destination) - { - NodeId = nodeId; - IsBitAddress = isBitAddress; - Destination = destination; + Endpoints = [endpoint]; } /// - /// Creates an End Point destination targeting multiple endpoints on a node. + /// Creates an endpoint destination targeting multiple endpoints on a node. /// - /// The NodeID of the destination. - /// The endpoint indices to target (each must be 1–7). - public EndPointDestination(byte nodeId, ReadOnlySpan endPoints) + public EndpointDestination(byte nodeId, ReadOnlySpan endpoints) { - byte bitMask = 0; - foreach (byte ep in endPoints) + if (endpoints.Length == 0) { - if (ep < 1 || ep > 7) - { - throw new ArgumentOutOfRangeException(nameof(endPoints), ep, "Bit-addressed endpoints must be between 1 and 7."); - } - - bitMask |= (byte)(1 << (ep - 1)); + throw new ArgumentException("At least one endpoint must be specified.", nameof(endpoints)); } NodeId = nodeId; - IsBitAddress = true; - Destination = bitMask; + Endpoints = endpoints.ToArray(); } /// @@ -101,14 +69,9 @@ public EndPointDestination(byte nodeId, ReadOnlySpan endPoints) public byte NodeId { get; } /// - /// Whether the destination is specified as a bit mask targeting multiple endpoints. + /// The endpoint indices on this node. /// - public bool IsBitAddress { get; } - - /// - /// The destination endpoint index (0–127) or bit mask (when is true). - /// - public byte Destination { get; } + public IReadOnlyList Endpoints { get; } } [CommandClass(CommandClassId.MultiChannelAssociation)] @@ -149,21 +112,8 @@ internal override async Task InterviewAsync(CancellationToken cancellationToken) protected override void ProcessUnsolicitedCommand(CommandClassFrame frame) { - switch ((MultiChannelAssociationCommand)frame.CommandId) - { - case MultiChannelAssociationCommand.Report: - { - MultiChannelAssociationReport report = MultiChannelAssociationReportCommand.Parse(frame, Logger); - UpdateGroupReport(report); - OnReportReceived?.Invoke(report); - break; - } - case MultiChannelAssociationCommand.SupportedGroupingsReport: - { - byte groupings = MultiChannelAssociationSupportedGroupingsReportCommand.Parse(frame, Logger); - SupportedGroupings = groupings; - break; - } - } + // Association Reports and Supported Groupings Reports are only sent in response + // to Get commands (per spec CC:008E.02.02.11.001 and CC:008E.02.05.11.001), + // so there are no unsolicited commands to handle. } }