Skip to content

Commit fd27117

Browse files
authored
Introduce IEndpoint abstraction (#180)
1 parent 40e229e commit fd27117

24 files changed

Lines changed: 108 additions & 70 deletions

‎.github/copilot-instructions.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ Implements the Z-Wave Serial API frame-level protocol (as defined by the Z-Wave
7373
### Command Classes Layer (`src/ZWave.CommandClasses/`)
7474

7575
Implements Z-Wave Command Classes (Z-Wave Application Specification). This project references `ZWave.Protocol` but **not** `ZWave.Serial`, enabling mock driver implementations without a serial dependency.
76-
- **`CommandClass`** / **`CommandClass<TCommand>`** — Abstract base classes. Each CC (e.g. `BinarySwitchCommandClass`) inherits from `CommandClass<TEnum>` where `TEnum` is a byte-backed enum of commands. Takes `IDriver` and `INode` interfaces (not concrete types).
77-
- **`IDriver`** / **`INode`** — Interfaces defined here that abstract the driver layer. `Driver` and `Node` implement these in the ZWave project.
76+
- **`IEndpoint`** — Interface representing a functional sub-unit of a node. Properties: `NodeId`, `EndpointIndex`, `CommandClasses`, `GetCommandClass()`. Endpoint 0 is the "Root Device" (the node itself); endpoints 1–127 are sub-devices discovered via Multi Channel CC.
77+
- **`INode`** — Extends `IEndpoint` with node-level properties (`FrequentListeningMode`). A node IS endpoint 0.
78+
- **`IDriver`** — Interface abstracting the driver layer. `SendCommandAsync` accepts `nodeId` and `endpointIndex` parameters.
79+
- **`CommandClass`** / **`CommandClass<TCommand>`** — Abstract base classes. Each CC (e.g. `BinarySwitchCommandClass`) inherits from `CommandClass<TEnum>` where `TEnum` is a byte-backed enum of commands. Takes `IDriver` and `IEndpoint` interfaces (not concrete types). The `Endpoint` property provides access to the endpoint this CC belongs to.
7880
- **`[CommandClass(CommandClassId.X)]` attribute** — Applied to each CC class. The source generator `CommandClassFactoryGenerator` scans for this attribute and generates `CommandClassFactory` with a mapping from `CommandClassId` → constructor.
7981
- **`ICommand` interface** (`CommandClasses/ICommand.cs`) — Different from the Serial API `ICommand`. Used for CC-level commands with `CommandClassId` and `CommandId`.
8082
- **`CommandClassFrame`** — Wraps CC payload bytes (CC ID + Command ID + parameters).
@@ -83,7 +85,7 @@ Implements Z-Wave Command Classes (Z-Wave Application Specification). This proje
8385

8486
- **`Driver`** — Entry point. Implements `IDriver`. Opens serial port, manages frame send/receive, processes unsolicited requests, coordinates request-response and callback flows.
8587
- **`Controller`** — Represents the Z-Wave USB controller. Runs identification sequence on startup.
86-
- **`Node`** — Represents a Z-Wave network node. Implements `INode`. Handles interviews and command class discovery. Node IDs are `ushort` throughout the codebase to support both classic (1–232) and Long Range (256+) nodes.
88+
- **`Node`** — Represents a Z-Wave network node. Implements `INode` (and thus `IEndpoint` with `EndpointIndex = 0`). Handles interviews and command class discovery. Node IDs are `ushort` throughout the codebase to support both classic (1–232) and Long Range (256+) nodes.
8789

8890
### Source Generators (`src/ZWave.BuildTools/`)
8991

@@ -116,7 +118,7 @@ Uses `Microsoft.Extensions.Logging` with source-generated `[LoggerMessage]` attr
116118

117119
**New Serial API Command**: Create a struct in `src/ZWave.Serial/Commands/` implementing `ICommand<T>` (and `IRequestWithCallback<T>` if it uses callbacks). Add the command ID to `CommandId` enum. Add tests in `src/ZWave.Serial.Tests/Commands/`. Node ID parameters are `ushort`; cast to `(byte)nodeId` when writing to the buffer. Use `CommandDataParsingHelpers` for shared parsing (node bitmasks, command class lists).
118120

119-
**New Command Class**: Create a class in `src/ZWave.CommandClasses/` inheriting `CommandClass<TEnum>`. Apply `[CommandClass(CommandClassId.X)]`. Define private inner structs for each command (Set/Get/Report) implementing `ICommand`. The source generator auto-registers it.
121+
**New Command Class**: Create a class in `src/ZWave.CommandClasses/` inheriting `CommandClass<TEnum>`. Apply `[CommandClass(CommandClassId.X)]`. Constructor takes `(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)`. Define private inner structs for each command (Set/Get/Report) implementing `ICommand`. The source generator auto-registers it. Use `Endpoint` property to access the endpoint (e.g. `Endpoint.NodeId`, `Endpoint.CommandClasses`).
120122

121123
## Protocol References
122124

‎.github/skills/zwave-implement-cc/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,9 @@ public sealed class {Name}CommandClass : CommandClass<{Name}Command>
226226
internal {Name}CommandClass(
227227
CommandClassInfo info,
228228
IDriver driver,
229-
INode node,
229+
IEndpoint endpoint,
230230
ILogger logger)
231-
: base(info, driver, node, logger)
231+
: base(info, driver, endpoint, logger)
232232
{
233233
}
234234

@@ -271,7 +271,7 @@ public sealed class {Name}CommandClass : CommandClass<{Name}Command>
271271
#### Key Points for the CC Class
272272

273273
- **`[CommandClass(CommandClassId.{Name})]` attribute**: This is required. A Roslyn source generator scans for this attribute and auto-generates the `CommandClassFactory` mapping. No other registration is needed.
274-
- **Constructor**: Always `internal`, takes `(CommandClassInfo info, IDriver driver, INode node, ILogger logger)`, calls `base(info, driver, node, logger)`.
274+
- **Constructor**: Always `internal`, takes `(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)`, calls `base(info, driver, endpoint, logger)`. The `Endpoint` property provides access to the endpoint this CC belongs to (e.g. `Endpoint.NodeId`, `Endpoint.CommandClasses`, `Endpoint.GetCommandClass()`).
275275
- **`IsCommandSupported`**: Return `true` for always-available commands, `false` for report-only/unsupported commands, and use `Version.HasValue ? Version >= N : null` for version-gated commands. Use `null` when it's unknown whether the command is supported.
276276
- **`Dependencies`**: Only override if this CC does NOT depend on the Version CC. The default is `{ CommandClassId.Version }`. The Version CC itself overrides this to `Array.Empty<CommandClassId>()`.
277277

‎src/ZWave.BuildTools/CommandClassFactoryGenerator.cs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,13 @@ namespace ZWave.CommandClasses;
103103
104104
internal static class CommandClassFactory
105105
{
106-
private static readonly Dictionary<CommandClassId, Func<CommandClassInfo, IDriver, INode, ILogger, CommandClass>> Constructors = new Dictionary<CommandClassId, Func<CommandClassInfo, IDriver, INode, ILogger, CommandClass>>
106+
private static readonly Dictionary<CommandClassId, Func<CommandClassInfo, IDriver, IEndpoint, ILogger, CommandClass>> Constructors = new Dictionary<CommandClassId, Func<CommandClassInfo, IDriver, IEndpoint, ILogger, CommandClass>>
107107
{
108108
");
109109

110110
foreach (KeyValuePair<string, string> pair in idToType)
111111
{
112-
sb.AppendLine($" {{ {pair.Key}, (info, driver, node, logger) => new {pair.Value}(info, driver, node, logger) }},");
112+
sb.AppendLine($" {{ {pair.Key}, (info, driver, endpoint, logger) => new {pair.Value}(info, driver, endpoint, logger) }},");
113113
}
114114

115115
sb.Append(@" };
@@ -125,10 +125,10 @@ internal static class CommandClassFactory
125125

126126
sb.Append(@" };
127127
128-
public static CommandClass Create(CommandClassInfo info, IDriver driver, INode node, ILogger logger)
129-
=> Constructors.TryGetValue(info.CommandClass, out Func<CommandClassInfo, IDriver, INode, ILogger, CommandClass>? constructor)
130-
? constructor(info, driver, node, logger)
131-
: new NotImplementedCommandClass(info, driver, node, logger);
128+
public static CommandClass Create(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)
129+
=> Constructors.TryGetValue(info.CommandClass, out Func<CommandClassInfo, IDriver, IEndpoint, ILogger, CommandClass>? constructor)
130+
? constructor(info, driver, endpoint, logger)
131+
: new NotImplementedCommandClass(info, driver, endpoint, logger);
132132
133133
public static CommandClassId GetCommandClassId<TCommandClass>()
134134
where TCommandClass : CommandClass

‎src/ZWave.CommandClasses/BasicCommandClass.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ public sealed class BasicCommandClass : CommandClass<BasicCommand>
4545
internal BasicCommandClass(
4646
CommandClassInfo info,
4747
IDriver driver,
48-
INode node,
48+
IEndpoint endpoint,
4949
ILogger logger)
50-
: base(info, driver, node, logger)
50+
: base(info, driver, endpoint, logger)
5151
{
5252
}
5353

‎src/ZWave.CommandClasses/BatteryCommandClass.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ public readonly record struct BatteryHealth(
164164
[CommandClass(CommandClassId.Battery)]
165165
public sealed class BatteryCommandClass : CommandClass<BatteryCommand>
166166
{
167-
public BatteryCommandClass(CommandClassInfo info, IDriver driver, INode node, ILogger logger)
168-
: base(info, driver, node, logger)
167+
public BatteryCommandClass(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)
168+
: base(info, driver, endpoint, logger)
169169
{
170170
}
171171

‎src/ZWave.CommandClasses/BinarySensorCommandClass.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ public sealed class BinarySensorCommandClass : CommandClass<BinarySensorCommand>
106106
{
107107
private Dictionary<BinarySensorType, bool?>? _sensorValues;
108108

109-
public BinarySensorCommandClass(CommandClassInfo info, IDriver driver, INode node, ILogger logger)
110-
: base(info, driver, node, logger)
109+
public BinarySensorCommandClass(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)
110+
: base(info, driver, endpoint, logger)
111111
{
112112
}
113113

‎src/ZWave.CommandClasses/BinarySwitchCommandClass.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public readonly record struct BinarySwitchReport(
4242
[CommandClass(CommandClassId.BinarySwitch)]
4343
public sealed class BinarySwitchCommandClass : CommandClass<BinarySwitchCommand>
4444
{
45-
internal BinarySwitchCommandClass(CommandClassInfo info, IDriver driver, INode node, ILogger logger)
46-
: base(info, driver, node, logger)
45+
internal BinarySwitchCommandClass(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)
46+
: base(info, driver, endpoint, logger)
4747
{
4848
}
4949

‎src/ZWave.CommandClasses/ColorSwitchCommandClass.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ public sealed class ColorSwitchCommandClass : CommandClass<ColorSwitchCommand>
9898
{
9999
private Dictionary<ColorSwitchColorComponent, ColorSwitchColorComponentState?>? _colorComponents;
100100

101-
public ColorSwitchCommandClass(CommandClassInfo info, IDriver driver, INode node, ILogger logger)
102-
: base(info, driver, node, logger)
101+
public ColorSwitchCommandClass(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)
102+
: base(info, driver, endpoint, logger)
103103
{
104104
}
105105

‎src/ZWave.CommandClasses/CommandClass.cs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ namespace ZWave.CommandClasses;
99
public abstract class CommandClass<TCommand> : CommandClass
1010
where TCommand : struct, Enum
1111
{
12-
internal CommandClass(CommandClassInfo info, IDriver driver, INode node, ILogger logger)
13-
: base(info, driver, node, logger)
12+
internal CommandClass(CommandClassInfo info, IDriver driver, IEndpoint endpoint, ILogger logger)
13+
: base(info, driver, endpoint, logger)
1414
{
1515
if (Unsafe.SizeOf<TCommand>() != Unsafe.SizeOf<byte>())
1616
{
@@ -52,12 +52,12 @@ private record struct AwaitedReport(
5252
internal CommandClass(
5353
CommandClassInfo info,
5454
IDriver driver,
55-
INode node,
55+
IEndpoint endpoint,
5656
ILogger logger)
5757
{
5858
Info = info;
5959
Driver = driver;
60-
Node = node;
60+
Endpoint = endpoint;
6161
Logger = logger;
6262
}
6363

@@ -72,9 +72,9 @@ internal CommandClass(
7272
protected IDriver Driver { get; }
7373

7474
/// <summary>
75-
/// Gets the node this command class belongs to.
75+
/// Gets the endpoint this command class belongs to.
7676
/// </summary>
77-
public INode Node { get; }
77+
public IEndpoint Endpoint { get; }
7878

7979
/// <summary>
8080
/// Gets the logger for this command class.
@@ -172,7 +172,7 @@ internal async Task SendCommandAsync<TRequest>(
172172
throw new ZWaveException(ZWaveErrorCode.CommandNotSupported, "This command is not supported by this node");
173173
}
174174

175-
await Driver.SendCommandAsync(command, Node.Id, cancellationToken).ConfigureAwait(false);
175+
await Driver.SendCommandAsync(command, Endpoint.NodeId, Endpoint.EndpointIndex, cancellationToken).ConfigureAwait(false);
176176
}
177177

178178
internal Task<CommandClassFrame> AwaitNextReportAsync<TReport>(CancellationToken cancellationToken)

‎src/ZWave.CommandClasses/IDriver.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
public interface IDriver
77
{
88
/// <summary>
9-
/// Sends a command class command to a specific node.
9+
/// Sends a command class command to a specific node and endpoint.
1010
/// </summary>
1111
Task SendCommandAsync<TCommand>(
1212
TCommand command,
1313
ushort nodeId,
14+
byte endpointIndex,
1415
CancellationToken cancellationToken)
1516
where TCommand : struct, ICommand;
1617

0 commit comments

Comments
 (0)