Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Both require `fetch-depth: 0` for Nerdbank.GitVersioning. There is no separate l
The foundational layer contains Z-Wave domain types shared across all projects, all in `namespace ZWave;`:
- **`CommandClassId`** — Enum of all Z-Wave command class IDs.
- **`CommandClassInfo`** — Record struct with CC ID, supported/controlled flags.
- **`ZWaveException`** / **`ZWaveErrorCode`** — Z-Wave-specific error types.
- **`ZWaveException`** / **`ZWaveErrorCode`** — Z-Wave-specific error types. `ZWaveException` has private constructors; use the `[DoesNotReturn]` static `ZWaveException.Throw(errorCode, message)` helpers instead of `throw new ZWaveException(...)`. Use `ArgumentNullException.ThrowIfNull()` and `ArgumentException.ThrowIfNullOrEmpty()` for argument validation.
- **`FrequentListeningMode`** / **`NodeType`** — Node classification enums.

### Serial API Layer (`src/ZWave.Serial/`)
Expand Down Expand Up @@ -116,7 +116,7 @@ Response structs that contain variable-length collections use count + indexer me
- **No `var`** — explicit types preferred (`csharp_style_var_*` = `false`).
- Allman-style braces (`csharp_new_line_before_open_brace = all`).
- NuGet package versions are centrally managed in `Directory.Packages.props`. When adding a package, add the version there and reference it without a version in the `.csproj`.
- `InternalsVisibleTo` is set: `ZWave.Serial` → `ZWave.Serial.Tests`, `ZWave.CommandClasses` → `ZWave` and `ZWave.CommandClasses.Tests`.
- `InternalsVisibleTo` is set: `ZWave.Protocol` → `ZWave.Serial`, `ZWave.Serial` → `ZWave.Serial.Tests`, `ZWave.CommandClasses` → `ZWave` and `ZWave.CommandClasses.Tests`.

## Testing Patterns

Expand Down
4 changes: 2 additions & 2 deletions .github/skills/zwave-implement-cc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Validation is performed in the report command's static `Parse` method. The `Pars

1. **Validates** the frame (e.g., minimum payload length, field value ranges)
2. **Logs a warning** via the `ILogger` parameter describing what's wrong
3. **Throws `ZWaveException(ZWaveErrorCode.InvalidPayload, ...)`** with a concise message
3. **Calls `ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, ...)`** with a concise message

The base class handles exception propagation differently depending on the report path:

Expand Down Expand Up @@ -524,7 +524,7 @@ internal readonly struct {Name}ReportCommand : ICommand
if (frame.CommandParameters.Length < 1)
{
logger.LogWarning("{Name} Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "{Name} Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "{Name} Report frame is too short");
}

ReadOnlySpan<byte> span = frame.CommandParameters.Span;
Expand Down
31 changes: 23 additions & 8 deletions src/Shared/BinaryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,30 @@ internal static class BinaryExtensions
/// Read a signed big-endian integer from a span of 1, 2, or 4 bytes.
/// </summary>
public static int ReadSignedVariableSizeBE(this ReadOnlySpan<byte> bytes)
=> bytes.Length switch
{
switch (bytes.Length)
{
1 => unchecked((sbyte)bytes[0]),
2 => BinaryPrimitives.ReadInt16BigEndian(bytes),
4 => BinaryPrimitives.ReadInt32BigEndian(bytes),
_ => throw new ZWaveException(
ZWaveErrorCode.InvalidPayload,
$"Invalid value size {bytes.Length}. Expected 1, 2, or 4."),
};
case 1:
{
return unchecked((sbyte)bytes[0]);
}
case 2:
{
return BinaryPrimitives.ReadInt16BigEndian(bytes);
}
case 4:
{
return BinaryPrimitives.ReadInt32BigEndian(bytes);
}
default:
{
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
$"Invalid value size {bytes.Length}. Expected 1, 2, or 4.");
return default;
}
}
}

/// <summary>
/// Get the minimum number of bytes (1, 2, or 4) needed to represent a signed integer.
Expand Down
4 changes: 2 additions & 2 deletions src/ZWave.CommandClasses/AssociationCommandClass.Report.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -144,7 +144,7 @@ public static (byte MaxNodesSupported, byte ReportsToFollow) ParseInto(
logger.LogWarning(
"Association Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Report frame is too short");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -72,7 +72,7 @@ public static byte Parse(CommandClassFrame frame, ILogger logger)
logger.LogWarning(
"Association Specific Group Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Specific Group Report frame is too short");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -69,7 +69,7 @@ public static byte Parse(CommandClassFrame frame, ILogger logger)
logger.LogWarning(
"Association Supported Groupings Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Supported Groupings Report frame is too short");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -139,7 +139,7 @@ public static (byte GroupingIdentifier, IReadOnlyList<AssociationGroupCommand> C
logger.LogWarning(
"Association Group Command List Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Group Command List Report frame is too short");
}
Expand All @@ -154,7 +154,7 @@ public static (byte GroupingIdentifier, IReadOnlyList<AssociationGroupCommand> C
"Association Group Command List Report frame is too short for declared list length ({DeclaredLength} bytes, but only {Available} available)",
listLength,
frame.CommandParameters.Length - 2);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Group Command List Report frame is too short for declared list length");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -162,7 +162,7 @@ public async Task<AssociationGroupInfo> GetGroupInfoAsync(byte groupingIdentifie
Logger.LogWarning(
"Association Group Info Report for group {GroupId} contained no group entries",
groupingIdentifier);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Group Info Report contained no group entries");
}
Expand Down Expand Up @@ -263,7 +263,7 @@ public static (bool DynamicInfo, List<AssociationGroupInfo> Groups) Parse(Comman
logger.LogWarning(
"Association Group Info Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Group Info Report frame is too short");
}
Expand All @@ -286,7 +286,7 @@ public static (bool DynamicInfo, List<AssociationGroupInfo> Groups) Parse(Comman
groupCount,
requiredLength,
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Group Info Report frame is too short for declared group count");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Text;
using System.Text;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;
Expand Down Expand Up @@ -88,7 +88,7 @@ public static (byte GroupingIdentifier, string Name) Parse(CommandClassFrame fra
logger.LogWarning(
"Association Group Name Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Group Name Report frame is too short");
}
Expand All @@ -103,7 +103,7 @@ public static (byte GroupingIdentifier, string Name) Parse(CommandClassFrame fra
"Association Group Name Report frame is too short for declared name length ({DeclaredLength} bytes, but only {Available} available)",
nameLength,
frame.CommandParameters.Length - 2);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Association Group Name Report frame is too short for declared name length");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -136,7 +136,7 @@ public static BarrierOperatorEventSignalReport Parse(CommandClassFrame frame, IL
logger.LogWarning(
"Barrier Operator Event Signaling Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Barrier Operator Event Signaling Report frame is too short");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -146,7 +146,7 @@ public static BarrierOperatorReport Parse(CommandClassFrame frame, ILogger logge
if (frame.CommandParameters.Length < 1)
{
logger.LogWarning("Barrier Operator Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Barrier Operator Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Barrier Operator Report frame is too short");
}

byte stateValue = frame.CommandParameters.Span[0];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -68,7 +68,7 @@ public static IReadOnlySet<BarrierOperatorSignalingSubsystemType> Parse(CommandC
logger.LogWarning(
"Barrier Operator Signal Supported Report frame is too short ({Length} bytes)",
frame.CommandParameters.Length);
throw new ZWaveException(
ZWaveException.Throw(
ZWaveErrorCode.InvalidPayload,
"Barrier Operator Signal Supported Report frame is too short");
}
Expand Down
4 changes: 2 additions & 2 deletions src/ZWave.CommandClasses/BasicCommandClass.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -171,7 +171,7 @@ public static BasicReport Parse(CommandClassFrame frame, ILogger logger)
if (frame.CommandParameters.Length < 1)
{
logger.LogWarning("Basic Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Basic Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Basic Report frame is too short");
}

GenericValue currentValue = frame.CommandParameters.Span[0];
Expand Down
8 changes: 4 additions & 4 deletions src/ZWave.CommandClasses/BatteryCommandClass.Health.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -85,7 +85,7 @@ public static BatteryHealth Parse(CommandClassFrame frame, ILogger logger)
if (frame.CommandParameters.Length < 2)
{
logger.LogWarning("Battery Health Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Battery Health Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Battery Health Report frame is too short");
}

// 0xff means unknown.
Expand All @@ -109,7 +109,7 @@ BatteryTemperatureScale batteryTemperatureScale
if (valueSize != 1 && valueSize != 2 && valueSize != 4)
{
logger.LogWarning("Battery Health Report has invalid Size value ({Size}). Expected 0, 1, 2, or 4", valueSize);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Battery Health Report has invalid Size value");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Battery Health Report has invalid Size value");
}

if (frame.CommandParameters.Length < 2 + valueSize)
Expand All @@ -118,7 +118,7 @@ BatteryTemperatureScale batteryTemperatureScale
"Battery Health Report frame value size ({ValueSize}) exceeds remaining bytes ({Remaining})",
valueSize,
frame.CommandParameters.Length - 2);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Battery Health Report frame is too short for declared value size");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Battery Health Report frame is too short for declared value size");
}

ReadOnlySpan<byte> valueBytes = frame.CommandParameters.Span.Slice(2, valueSize);
Expand Down
4 changes: 2 additions & 2 deletions src/ZWave.CommandClasses/BatteryCommandClass.Report.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -115,7 +115,7 @@ public static BatteryReport Parse(CommandClassFrame frame, ILogger logger)
if (frame.CommandParameters.Length < 1)
{
logger.LogWarning("Battery Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Battery Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Battery Report frame is too short");
}

BatteryLevel batteryLevel = frame.CommandParameters.Span[0];
Expand Down
4 changes: 2 additions & 2 deletions src/ZWave.CommandClasses/BinarySensorCommandClass.Report.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -115,7 +115,7 @@ public static BinarySensorReport Parse(CommandClassFrame frame, ILogger logger)
if (frame.CommandParameters.Length < 1)
{
logger.LogWarning("Binary Sensor Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Binary Sensor Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Binary Sensor Report frame is too short");
}

bool sensorValue = frame.CommandParameters.Span[0] == 0xff;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -75,7 +75,7 @@ public static IReadOnlySet<BinarySensorType> Parse(CommandClassFrame frame, ILog
if (frame.CommandParameters.Length < 1)
{
logger.LogWarning("Binary Sensor Supported Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Binary Sensor Supported Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Binary Sensor Supported Report frame is too short");
}

return BitMaskHelper.ParseBitMask<BinarySensorType>(frame.CommandParameters.Span);
Expand Down
4 changes: 2 additions & 2 deletions src/ZWave.CommandClasses/BinarySwitchCommandClass.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -177,7 +177,7 @@ public static BinarySwitchReport Parse(CommandClassFrame frame, ILogger logger)
if (frame.CommandParameters.Length < 1)
{
logger.LogWarning("Binary Switch Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Binary Switch Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Binary Switch Report frame is too short");
}

GenericValue currentValue = frame.CommandParameters.Span[0];
Expand Down
7 changes: 4 additions & 3 deletions src/ZWave.CommandClasses/ClockCommandClass.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -225,7 +225,7 @@ public static ClockReport Parse(CommandClassFrame frame, ILogger logger)
if (frame.CommandParameters.Length < 2)
{
logger.LogWarning("Clock Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Clock Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Clock Report frame is too short");
}

ReadOnlySpan<byte> span = frame.CommandParameters.Span;
Expand All @@ -240,7 +240,8 @@ public static ClockReport Parse(CommandClassFrame frame, ILogger logger)
catch (ArgumentOutOfRangeException)
{
logger.LogWarning("Clock Report has invalid time values (hour={Hour}, minute={Minute})", hour, minute);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Clock Report has invalid time values");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Clock Report has invalid time values");
return default;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/ZWave.CommandClasses/ColorSwitchCommandClass.Report.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;

namespace ZWave.CommandClasses;

Expand Down Expand Up @@ -112,7 +112,7 @@ public static ColorSwitchReport Parse(CommandClassFrame frame, ILogger logger)
if (frame.CommandParameters.Length < 2)
{
logger.LogWarning("Color Switch Report frame is too short ({Length} bytes)", frame.CommandParameters.Length);
throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Color Switch Report frame is too short");
ZWaveException.Throw(ZWaveErrorCode.InvalidPayload, "Color Switch Report frame is too short");
}

ReadOnlySpan<byte> span = frame.CommandParameters.Span;
Expand Down
Loading