Skip to content

Commit 56ff69a

Browse files
committed
Implement Multicast
1 parent c368780 commit 56ff69a

File tree

14 files changed

+216
-132
lines changed

14 files changed

+216
-132
lines changed

Examples.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,18 @@ async Task Meter_Update(Node sender, CommandClassEventArgs args)
6363
}
6464
```
6565

66-
#### Broadcasting Commands:
66+
#### Broadcast Commands:
6767
The controller contains a broadcast node with a set of command classes prepopulated. Broadcast commands do not reflect what command classes the network may or may not support.
6868
```c#
6969
await controller.BroadcastNode.GetCommandClass<SwitchBinary>()!.Set(true);
7070
```
71-
_This example turns on all switches in the network_
71+
_This example turns on all switches in the network_
72+
73+
#### Multicast Commands:
74+
The controller can create Multicast Groups of multiple Nodes. Multicast Command Classes will be the minimum set supported by all group members.
75+
```c#
76+
1: NodeGroup group = controller.CreateGroup(node1, node2);
77+
2: await group.GetCommandClass<SwitchBinary>()!.Set(true, TimeSpan.FromSeconds(1));
78+
```
79+
_Line 1 Creates a group with two members._\
80+
_Line 2 Turn on all binary switches in the group with a single multicast command._

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ An implementation of ZWave Plus using the 2024a public specification.
1717
* See our [Examples Page](Examples.md)
1818

1919
#### Not Supported:
20-
* Multicast is not fully implemented (including secure multicast)
20+
* Secure Multicast is not fully implemented
2121

2222
#### Other Projects
2323
* Check out my other projects for [HomeKit](https://github.com/SmartHomeOS/HomeKitDotNet) and [Matter](https://github.com/SmartHomeOS/MatterDotNet).

ZWaveDotNet/CommandClasses/CommandClassBase.cs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ public abstract class CommandClassBase
4949
/// Parent Controller
5050
/// </summary>
5151
protected Controller controller;
52-
private CommandClass commandClass;
53-
private byte endpoint;
54-
private ConcurrentDictionary<byte, BlockingCollection<TaskCompletionSource<ReportMessage>>> callbacks = new ConcurrentDictionary<byte, BlockingCollection<TaskCompletionSource<ReportMessage>>>();
52+
private readonly CommandClass commandClass;
53+
private readonly byte endpoint;
54+
private readonly ConcurrentDictionary<byte, BlockingCollection<TaskCompletionSource<ReportMessage>>> callbacks = new ConcurrentDictionary<byte, BlockingCollection<TaskCompletionSource<ReportMessage>>>();
5555

5656
/// <summary>
5757
/// Base Class
@@ -319,10 +319,26 @@ protected async Task<bool> SendCommand(Enum command, CancellationToken token, pa
319319
/// <param name="supervised"></param>
320320
/// <param name="payload"></param>
321321
/// <returns></returns>
322-
protected async Task<bool> SendCommand(Enum command, CancellationToken token, bool supervised = false, params byte[] payload)
322+
protected Task<bool> SendCommand(Enum command, CancellationToken token, bool supervised = false, params byte[] payload)
323323
{
324-
// TODO - Multicast
325-
CommandMessage data = new CommandMessage(controller, node.ID, endpoint, commandClass, Convert.ToByte(command), supervised, payload);
324+
return SendCommand(Convert.ToByte(command), token, supervised, payload);
325+
}
326+
327+
/// <summary>
328+
/// Send a command (no response expected)
329+
/// </summary>
330+
/// <param name="command"></param>
331+
/// <param name="token"></param>
332+
/// <param name="supervised"></param>
333+
/// <param name="payload"></param>
334+
/// <returns></returns>
335+
protected async Task<bool> SendCommand(byte command, CancellationToken token, bool supervised = false, params byte[] payload)
336+
{
337+
CommandMessage data;
338+
if (node is NodeGroup group)
339+
data = new CommandMessage(controller, group.MemberIDs, 0, commandClass, command, supervised, payload);
340+
else
341+
data = new CommandMessage(controller, [node.ID], endpoint, commandClass, Convert.ToByte(command), supervised, payload);
326342
return await SendCommand(data, token).ConfigureAwait(false);
327343
}
328344

@@ -339,9 +355,7 @@ internal async Task<bool> SendCommand(CommandMessage data, CancellationToken tok
339355
{
340356
if (controller.SecurityManager == null)
341357
throw new InvalidOperationException("Secure command requires security manager");
342-
SecurityManager.NetworkKey? key = controller.SecurityManager.GetHighestKey(node.ID);
343-
if (key == null)
344-
throw new InvalidOperationException($"Command classes are secure but no keys exist for node {node.ID}");
358+
SecurityManager.NetworkKey key = controller.SecurityManager.GetHighestKey(node.ID) ?? throw new InvalidOperationException($"Command classes are secure but no keys exist for node {node.ID}");
345359
if (key.Key == SecurityManager.RecordType.S0)
346360
await node.GetCommandClass<Security0>()!.Encapsulate(data.Payload, token).ConfigureAwait(false);
347361
else if (key.Key > SecurityManager.RecordType.S0)
@@ -350,10 +364,12 @@ internal async Task<bool> SendCommand(CommandMessage data, CancellationToken tok
350364
throw new InvalidOperationException("Security required but no keys are available");
351365
}
352366

353-
DataMessage message = data.ToMessage();
367+
CallbackBase message = data.ToMessage();
354368
if ((!node.LongRange && data.Payload.Count > 46) || (data.Payload.Count > 120))
355369
{
356-
return await TransportService.Transmit(message, token);
370+
if (message is DataMessage msg)
371+
return await TransportService.Transmit(msg, token);
372+
throw new InvalidOperationException("Multicast Transport Commands are not supported");
357373
}
358374

359375
for (int i = 0; i < 3; i++)
@@ -374,7 +390,7 @@ internal async Task<bool> SendCommand(CommandMessage data, CancellationToken tok
374390
/// <param name="ex"></param>
375391
/// <returns></returns>
376392
/// <exception cref="Exception"></exception>
377-
internal async Task<bool> AttemptTransmission(DataMessage message, CancellationToken cancellationToken, bool ex = false)
393+
internal async Task<bool> AttemptTransmission(CallbackBase message, CancellationToken cancellationToken, bool ex = false)
378394
{
379395
DataCallback dc = await controller.Flow.SendAcknowledgedResponseCallback(message, b => b != 0x0, cancellationToken).ConfigureAwait(false);
380396
if (dc.Status != TransmissionStatus.CompleteOk && dc.Status != TransmissionStatus.CompleteNoAck && dc.Status != TransmissionStatus.CompleteVerified)
@@ -471,8 +487,10 @@ internal Task<ReportMessage> Receive(Enum response, CancellationToken token)
471487
cbList.Add(src, token);
472488
else
473489
{
474-
BlockingCollection<TaskCompletionSource<ReportMessage>> newCallbacks = new BlockingCollection<TaskCompletionSource<ReportMessage>>();
475-
newCallbacks.Add(src, token);
490+
BlockingCollection<TaskCompletionSource<ReportMessage>> newCallbacks = new BlockingCollection<TaskCompletionSource<ReportMessage>>
491+
{
492+
{ src, token }
493+
};
476494
if (!callbacks.TryAdd(cmd, newCallbacks))
477495
callbacks[cmd].Add(src, token);
478496
}

ZWaveDotNet/CommandClasses/NoOperation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal NoOperation(Node node, byte endpoint) : base(node, endpoint, CommandCla
2626

2727
public async Task<bool> Ping(CancellationToken cancellationToken = default)
2828
{
29-
CommandMessage data = new CommandMessage(controller, node.ID, (byte)(EndPoint & 0x7F), CommandClass, 0x0);
29+
CommandMessage data = new CommandMessage(controller, [node.ID], (byte)(EndPoint & 0x7F), CommandClass, 0x0);
3030
data.Payload.RemoveAt(1); //This class sends no command
3131
DataCallback dc = await controller.Flow.SendAcknowledgedResponseCallback(data.ToMessage(), b => b != 0x0, cancellationToken);
3232
return (dc.Status == TransmissionStatus.CompleteOk || dc.Status == TransmissionStatus.CompleteNoAck || dc.Status == TransmissionStatus.CompleteVerified);

ZWaveDotNet/CommandClasses/Security0.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ internal async Task SchemeGet(CancellationToken cancellationToken = default)
6666
internal async Task KeySet(CancellationToken cancellationToken = default)
6767
{
6868
Log.Verbose($"Setting Network Key on {node.ID}");
69-
CommandMessage data = new CommandMessage(controller, node.ID, EndPoint, CommandClass, (byte)Security0Command.NetworkKeySet, false, controller.NetworkKeyS0);
69+
CommandMessage data = new CommandMessage(controller, [node.ID], EndPoint, CommandClass, (byte)Security0Command.NetworkKeySet, false, controller.NetworkKeyS0);
7070
await TransmitTemp(data.Payload, cancellationToken).ConfigureAwait(false);
7171
}
7272

@@ -95,7 +95,7 @@ internal async Task TransmitTemp(List<byte> payload, CancellationToken cancellat
9595
Log.Verbose("Creating Temp Payload for " + node.ID.ToString());
9696
byte[] receiversNonce = report.Payload.ToArray();
9797
byte[] sendersNonce = new byte[8];
98-
new Random().NextBytes(sendersNonce);
98+
Random.Shared.NextBytes(sendersNonce);
9999
payload.Insert(0, 0x0); //Sequenced = False
100100
byte[] encrypted = EncryptDecryptPayload(payload.ToArray(), sendersNonce, receiversNonce, controller.tempE);
101101
byte[] mac = AES.ComputeMAC(controller.ID, node.ID, (byte)Security0Command.MessageEncap, sendersNonce, receiversNonce, encrypted, controller.tempA);
@@ -118,7 +118,7 @@ internal async Task Encapsulate(List<byte> payload, CancellationToken cancellati
118118
Log.Verbose("Creating Payload for " + node.ID.ToString());
119119
byte[] receiversNonce = report.Payload.ToArray();
120120
byte[] sendersNonce = new byte[8];
121-
new Random().NextBytes(sendersNonce);
121+
Random.Shared.NextBytes(sendersNonce);
122122
payload.Insert(0, 0x0); //Sequenced = False
123123
byte[] encrypted = EncryptDecryptPayload(payload.ToArray(), sendersNonce, receiversNonce, controller.EncryptionKey);
124124
byte[] mac = AES.ComputeMAC(controller.ID, node.ID, (byte)Security0Command.MessageEncap, sendersNonce, receiversNonce, encrypted, controller.AuthenticationKey);

ZWaveDotNet/CommandClasses/Security2.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class Security2 : CommandClassBase
3434
private const byte TRANSFER_COMPLETE = 0x1;
3535
public event CommandClassEvent<ErrorReport>? SecurityError;
3636
TaskCompletionSource bootstrapComplete = new TaskCompletionSource();
37-
private static uint sequence = (uint)new Random().Next();
37+
private static uint sequence = (uint)Random.Shared.Next();
3838

3939
internal enum Security2Command
4040
{
@@ -128,7 +128,7 @@ internal async Task KexFail(KexFailType type, CancellationToken cancellationToke
128128
controller.SecurityManager?.GetRequestedKeys(node.ID, true);
129129
if (type == KexFailType.KEX_FAIL_AUTH || type == KexFailType.KEX_FAIL_DECRYPT || type == KexFailType.KEX_FAIL_KEY_VERIFY || type == KexFailType.KEX_FAIL_KEY_GET)
130130
{
131-
CommandMessage reportKex = new CommandMessage(controller, node.ID, EndPoint, CommandClass, (byte)Security2Command.KEXFail, false, (byte)type);
131+
CommandMessage reportKex = new CommandMessage(controller, [node.ID], EndPoint, CommandClass, (byte)Security2Command.KEXFail, false, (byte)type);
132132
await Transmit(reportKex.Payload, SecurityManager.RecordType.ECDH_TEMP, cancellationToken).ConfigureAwait(false);
133133
}
134134
else
@@ -386,6 +386,8 @@ internal override async Task<SupervisionStatus> Handle(ReportMessage message)
386386
{
387387
if (controller.SecurityManager == null)
388388
return SupervisionStatus.Fail;
389+
if (message.IsMulticastMethod)
390+
return SupervisionStatus.Fail;
389391
KeyExchangeReport? requestedKeys = controller.SecurityManager.GetRequestedKeys(node.ID);
390392
if (requestedKeys != null)
391393
{
@@ -396,7 +398,7 @@ internal override async Task<SupervisionStatus> Handle(ReportMessage message)
396398
}
397399
requestedKeys.Echo = true;
398400
Log.Verbose("Responding: " + requestedKeys.ToString());
399-
CommandMessage reportKex = new CommandMessage(controller, node.ID, EndPoint, CommandClass, (byte)Security2Command.KEXReport, false, requestedKeys.ToBytes());
401+
CommandMessage reportKex = new CommandMessage(controller, [node.ID], EndPoint, CommandClass, (byte)Security2Command.KEXReport, false, requestedKeys.ToBytes());
400402
await Transmit(reportKex.Payload, SecurityManager.RecordType.ECDH_TEMP).ConfigureAwait(false);
401403
}
402404
}
@@ -445,13 +447,15 @@ internal override async Task<SupervisionStatus> Handle(ReportMessage message)
445447
default:
446448
return SupervisionStatus.Fail; //Invalid Key Type - Ignore this
447449
}
448-
CommandMessage data = new CommandMessage(controller, node.ID, EndPoint, CommandClass, (byte)Security2Command.NetworkKeyReport, false, resp);
450+
CommandMessage data = new CommandMessage(controller, [node.ID], EndPoint, CommandClass, (byte)Security2Command.NetworkKeyReport, false, resp);
449451
await Transmit(data.Payload, SecurityManager.RecordType.ECDH_TEMP).ConfigureAwait(false);
450452
Log.Verbose($"Provided Network Key {key}");
451453
return SupervisionStatus.Success;
452454
case Security2Command.NetworkKeyVerify:
453455
if (controller.SecurityManager == null)
454456
return SupervisionStatus.Fail;
457+
if (message.IsMulticastMethod)
458+
return SupervisionStatus.Fail;
455459
Log.Verbose("Network Key Verified!");
456460
if (message.SecurityLevel == SecurityKey.None || (message.Flags & ReportFlags.Security) != ReportFlags.Security)
457461
{
@@ -460,7 +464,7 @@ internal override async Task<SupervisionStatus> Handle(ReportMessage message)
460464
}
461465
Log.Information($"Revoking {message.SecurityLevel}");
462466
controller.SecurityManager.RevokeKey(node.ID, SecurityManager.KeyToType(message.SecurityLevel));
463-
CommandMessage transferEnd = new CommandMessage(controller, node.ID, EndPoint, CommandClass, (byte)Security2Command.TransferEnd, false, KEY_VERIFIED);
467+
CommandMessage transferEnd = new CommandMessage(controller, [node.ID], EndPoint, CommandClass, (byte)Security2Command.TransferEnd, false, KEY_VERIFIED);
464468
await Transmit(transferEnd.Payload, SecurityManager.RecordType.ECDH_TEMP);
465469
return SupervisionStatus.Success;
466470
case Security2Command.NonceGet:
@@ -534,6 +538,8 @@ internal override async Task<SupervisionStatus> Handle(ReportMessage message)
534538
bootstrapComplete.TrySetResult();
535539
return SupervisionStatus.Success;
536540
case Security2Command.KEXFail:
541+
if (message.IsMulticastMethod)
542+
return SupervisionStatus.Fail;
537543
ErrorReport errorMessage = new ErrorReport(message.Payload.Span[0], ((KexFailType)message.Payload.Span[0]).ToString());
538544
Log.Error("Key Exchange Failure " + errorMessage);
539545
await FireEvent(SecurityError, errorMessage);

ZWaveDotNet/CommandClasses/TransportService.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ internal static async Task<bool> Transmit (DataMessage message, CancellationToke
8181
bool success = true;
8282
for (int i = 0; i < message.Data.Count; i += MAX_SEGMENT)
8383
{
84-
DataMessage segment = new DataMessage(message.controller, message.DestinationNodeID, message.Data.GetRange(i, Math.Min(message.Data.Count, i + MAX_SEGMENT) - i), true, (message.Options & TransmitOptions.ExploreNPDUs) == TransmitOptions.ExploreNPDUs);
85-
success &= await message.controller.Nodes[message.SourceNodeID].GetCommandClass<TransportService>()!.TransmitSegment(segment, sessionId, i, message.Data.Count, token);
84+
DataMessage segment = new DataMessage(message.Controller, message.DestinationNodeID, message.Data.GetRange(i, Math.Min(message.Data.Count, i + MAX_SEGMENT) - i), true, (message.Options & TransmitOptions.ExploreNPDUs) == TransmitOptions.ExploreNPDUs);
85+
success &= await message.Controller.Nodes[message.SourceNodeID].GetCommandClass<TransportService>()!.TransmitSegment(segment, sessionId, i, message.Data.Count, token);
8686
}
8787
return success;
8888
}
@@ -231,8 +231,8 @@ internal override async Task<SupervisionStatus> Handle(ReportMessage msg)
231231
int offset = ((0x7 & msg.Payload.Span[0] << 8) | msg.Payload.Span[1]);
232232
Log.Information("Retransmitting segment " + offset + " for session " + (msg.Payload.Span[0] >> 4));
233233

234-
DataMessage segment = new DataMessage(message.controller, message.DestinationNodeID, message.Data.GetRange(offset, Math.Min(message.Data.Count, offset + MAX_SEGMENT) - offset), true, (message.Options & TransmitOptions.ExploreNPDUs) == TransmitOptions.ExploreNPDUs);
235-
await message.controller.Nodes[message.SourceNodeID].GetCommandClass<TransportService>()!.TransmitSegment(segment, (byte)(msg.Payload.Span[0] >> 4), offset, message.Data.Count);
234+
DataMessage segment = new DataMessage(message.Controller, message.DestinationNodeID, message.Data.GetRange(offset, Math.Min(message.Data.Count, offset + MAX_SEGMENT) - offset), true, (message.Options & TransmitOptions.ExploreNPDUs) == TransmitOptions.ExploreNPDUs);
235+
await message.Controller.Nodes[message.SourceNodeID].GetCommandClass<TransportService>()!.TransmitSegment(segment, (byte)(msg.Payload.Span[0] >> 4), offset, message.Data.Count);
236236
}
237237
break;
238238
}

ZWaveDotNet/Entities/Controller.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public class Controller : IDisposable
104104
private readonly List<Memory<byte>> provisionList = new List<Memory<byte>>();
105105
private static readonly SemaphoreSlim nodeListLock = new SemaphoreSlim(1, 1);
106106
private CancellationTokenSource running = new CancellationTokenSource();
107+
private volatile byte GroupID = 1;
107108

108109
/// <summary>
109110
/// Create a new controller for the given serial port
@@ -800,6 +801,22 @@ public async Task<bool> RemoveFailedNode(ushort nodeId, CancellationToken cancel
800801
return (int)dc.Status == 0x1;
801802
}
802803

804+
/// <summary>
805+
/// Create a Node Group which supports Multicast Commands to all members
806+
/// </summary>
807+
/// <param name="nodes"></param>
808+
/// <returns></returns>
809+
/// <exception cref="ArgumentException"></exception>
810+
public NodeGroup CreateGroup(params Node[] nodes)
811+
{
812+
if (nodes == null || nodes.Length == 0)
813+
throw new ArgumentException("Cannot create a group without members");
814+
NodeGroup group = new NodeGroup(GroupID++, this, nodes[0]);
815+
for (int i = 1; i < nodes.Length; i++)
816+
group.AddNode(nodes[i]);
817+
return group;
818+
}
819+
803820
private async Task<bool> BootstrapUnsecure(Node node)
804821
{
805822
await Task.Delay(1000); //Give including node a chance to get ready

0 commit comments

Comments
 (0)