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
61 changes: 61 additions & 0 deletions src/Shared/BinaryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,65 @@ internal static class BinaryExtensions
public static void WriteBytesBE(this uint value, Span<byte> destination) => BinaryPrimitives.WriteUInt32BigEndian(destination, value);

public static int ToInt32BE(this ReadOnlySpan<byte> bytes) => BinaryPrimitives.ReadInt32BigEndian(bytes);

public static void WriteBytesBE(this int value, Span<byte> destination) => BinaryPrimitives.WriteInt32BigEndian(destination, value);

/// <summary>
/// Lookup table for 10^n where n is a Z-Wave precision value (0–7).
/// </summary>
public static ReadOnlySpan<double> PowersOfTen => [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000];

/// <summary>
/// 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
{
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."),
};

/// <summary>
/// Get the minimum number of bytes (1, 2, or 4) needed to represent a signed integer.
/// </summary>
public static int GetSignedVariableSize(this int value)
=> value switch
{
>= sbyte.MinValue and <= sbyte.MaxValue => 1,
>= short.MinValue and <= short.MaxValue => 2,
_ => 4,
};

/// <summary>
/// Write a signed big-endian integer using 1, 2, or 4 bytes based on the destination length.
/// </summary>
public static void WriteSignedVariableSizeBE(this int value, Span<byte> destination)
{
switch (destination.Length)
{
case 1:
{
destination[0] = unchecked((byte)(sbyte)value);
break;
}
case 2:
{
BinaryPrimitives.WriteInt16BigEndian(destination, (short)value);
break;
}
case 4:
{
BinaryPrimitives.WriteInt32BigEndian(destination, value);
break;
}
default:
{
throw new ArgumentException($"Invalid destination size {destination.Length}. Expected 1, 2, or 4.", nameof(destination));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using Microsoft.Extensions.Logging.Abstractions;

namespace ZWave.CommandClasses.Tests;

public partial class HumidityControlModeCommandClassTests
{
[TestMethod]
public void GetCommand_Create_HasCorrectFormat()
{
HumidityControlModeCommandClass.HumidityControlModeGetCommand command =
HumidityControlModeCommandClass.HumidityControlModeGetCommand.Create();

Assert.AreEqual(CommandClassId.HumidityControlMode, HumidityControlModeCommandClass.HumidityControlModeGetCommand.CommandClassId);
Assert.AreEqual((byte)HumidityControlModeCommand.Get, HumidityControlModeCommandClass.HumidityControlModeGetCommand.CommandId);
Assert.AreEqual(2, command.Frame.Data.Length);
}

[TestMethod]
public void SetCommand_Create_Off()
{
HumidityControlModeCommandClass.HumidityControlModeSetCommand command =
HumidityControlModeCommandClass.HumidityControlModeSetCommand.Create(HumidityControlMode.Off);

Assert.AreEqual(CommandClassId.HumidityControlMode, HumidityControlModeCommandClass.HumidityControlModeSetCommand.CommandClassId);
Assert.AreEqual((byte)HumidityControlModeCommand.Set, HumidityControlModeCommandClass.HumidityControlModeSetCommand.CommandId);
Assert.AreEqual(3, command.Frame.Data.Length);
Assert.AreEqual(0x00, command.Frame.CommandParameters.Span[0]);
}

[TestMethod]
public void SetCommand_Create_Humidify()
{
HumidityControlModeCommandClass.HumidityControlModeSetCommand command =
HumidityControlModeCommandClass.HumidityControlModeSetCommand.Create(HumidityControlMode.Humidify);

Assert.AreEqual(0x01, command.Frame.CommandParameters.Span[0]);
}

[TestMethod]
public void SetCommand_Create_Dehumidify()
{
HumidityControlModeCommandClass.HumidityControlModeSetCommand command =
HumidityControlModeCommandClass.HumidityControlModeSetCommand.Create(HumidityControlMode.Dehumidify);

Assert.AreEqual(0x02, command.Frame.CommandParameters.Span[0]);
}

[TestMethod]
public void SetCommand_Create_Auto()
{
HumidityControlModeCommandClass.HumidityControlModeSetCommand command =
HumidityControlModeCommandClass.HumidityControlModeSetCommand.Create(HumidityControlMode.Auto);

Assert.AreEqual(0x03, command.Frame.CommandParameters.Span[0]);
}

[TestMethod]
public void SetCommand_Create_ReservedBitsClear()
{
// Ensure upper 4 bits are always zero per spec
HumidityControlModeCommandClass.HumidityControlModeSetCommand command =
HumidityControlModeCommandClass.HumidityControlModeSetCommand.Create(HumidityControlMode.Auto);

Assert.AreEqual(0x00, command.Frame.CommandParameters.Span[0] & 0xF0);
}

[TestMethod]
public void Report_Parse_Off()
{
// CC=0x6D, Cmd=0x03, Mode=0x00 (Off)
byte[] data = [0x6D, 0x03, 0x00];
CommandClassFrame frame = new(data);

HumidityControlModeReport report = HumidityControlModeCommandClass.HumidityControlModeReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlMode.Off, report.Mode);
}

[TestMethod]
public void Report_Parse_Humidify()
{
byte[] data = [0x6D, 0x03, 0x01];
CommandClassFrame frame = new(data);

HumidityControlModeReport report = HumidityControlModeCommandClass.HumidityControlModeReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlMode.Humidify, report.Mode);
}

[TestMethod]
public void Report_Parse_Dehumidify()
{
byte[] data = [0x6D, 0x03, 0x02];
CommandClassFrame frame = new(data);

HumidityControlModeReport report = HumidityControlModeCommandClass.HumidityControlModeReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlMode.Dehumidify, report.Mode);
}

[TestMethod]
public void Report_Parse_Auto()
{
byte[] data = [0x6D, 0x03, 0x03];
CommandClassFrame frame = new(data);

HumidityControlModeReport report = HumidityControlModeCommandClass.HumidityControlModeReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlMode.Auto, report.Mode);
}

[TestMethod]
public void Report_Parse_ReservedBitsIgnored()
{
// Upper 4 bits are reserved and should be ignored
byte[] data = [0x6D, 0x03, 0xF2];
CommandClassFrame frame = new(data);

HumidityControlModeReport report = HumidityControlModeCommandClass.HumidityControlModeReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlMode.Dehumidify, report.Mode);
}

[TestMethod]
public void Report_Parse_TooShort_Throws()
{
byte[] data = [0x6D, 0x03];
CommandClassFrame frame = new(data);

Assert.Throws<ZWaveException>(
() => HumidityControlModeCommandClass.HumidityControlModeReportCommand.Parse(frame, NullLogger.Instance));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Microsoft.Extensions.Logging.Abstractions;

namespace ZWave.CommandClasses.Tests;

public partial class HumidityControlModeCommandClassTests
{
[TestMethod]
public void SupportedGetCommand_Create_HasCorrectFormat()
{
HumidityControlModeCommandClass.HumidityControlModeSupportedGetCommand command =
HumidityControlModeCommandClass.HumidityControlModeSupportedGetCommand.Create();

Assert.AreEqual(CommandClassId.HumidityControlMode, HumidityControlModeCommandClass.HumidityControlModeSupportedGetCommand.CommandClassId);
Assert.AreEqual((byte)HumidityControlModeCommand.SupportedGet, HumidityControlModeCommandClass.HumidityControlModeSupportedGetCommand.CommandId);
Assert.AreEqual(2, command.Frame.Data.Length);
}

[TestMethod]
public void SupportedReport_Parse_HumidifyAndDehumidify()
{
// CC=0x6D, Cmd=0x05, BitMask=0x06 (bits 1,2 = Humidify, Dehumidify)
byte[] data = [0x6D, 0x05, 0x06];
CommandClassFrame frame = new(data);

IReadOnlySet<HumidityControlMode> supported = HumidityControlModeCommandClass.HumidityControlModeSupportedReportCommand.Parse(frame, NullLogger.Instance);

Assert.HasCount(2, supported);
Assert.Contains(HumidityControlMode.Humidify, supported);
Assert.Contains(HumidityControlMode.Dehumidify, supported);
}

[TestMethod]
public void SupportedReport_Parse_AllModes()
{
// CC=0x6D, Cmd=0x05, BitMask=0x0E (bits 1,2,3 = Humidify, Dehumidify, Auto)
byte[] data = [0x6D, 0x05, 0x0E];
CommandClassFrame frame = new(data);

IReadOnlySet<HumidityControlMode> supported = HumidityControlModeCommandClass.HumidityControlModeSupportedReportCommand.Parse(frame, NullLogger.Instance);

Assert.HasCount(3, supported);
Assert.Contains(HumidityControlMode.Humidify, supported);
Assert.Contains(HumidityControlMode.Dehumidify, supported);
Assert.Contains(HumidityControlMode.Auto, supported);
}

[TestMethod]
public void SupportedReport_Parse_ReservedBit0Ignored()
{
// Bit 0 is reserved per spec and should be ignored by the receiver.
// CC=0x6D, Cmd=0x05, BitMask=0x07 (bits 0,1,2 — bit 0 reserved)
byte[] data = [0x6D, 0x05, 0x07];
CommandClassFrame frame = new(data);

IReadOnlySet<HumidityControlMode> supported = HumidityControlModeCommandClass.HumidityControlModeSupportedReportCommand.Parse(frame, NullLogger.Instance);

Assert.HasCount(2, supported);
Assert.Contains(HumidityControlMode.Humidify, supported);
Assert.Contains(HumidityControlMode.Dehumidify, supported);
}

[TestMethod]
public void SupportedReport_Parse_EmptyMask()
{
// CC=0x6D, Cmd=0x05, BitMask=0x00
byte[] data = [0x6D, 0x05, 0x00];
CommandClassFrame frame = new(data);

IReadOnlySet<HumidityControlMode> supported = HumidityControlModeCommandClass.HumidityControlModeSupportedReportCommand.Parse(frame, NullLogger.Instance);

Assert.IsEmpty(supported);
}

[TestMethod]
public void SupportedReport_Parse_MultipleMaskBytes()
{
// Two mask bytes - forward compatible
// CC=0x6D, Cmd=0x05, Mask1=0x06, Mask2=0x00
byte[] data = [0x6D, 0x05, 0x06, 0x00];
CommandClassFrame frame = new(data);

IReadOnlySet<HumidityControlMode> supported = HumidityControlModeCommandClass.HumidityControlModeSupportedReportCommand.Parse(frame, NullLogger.Instance);

Assert.HasCount(2, supported);
Assert.Contains(HumidityControlMode.Humidify, supported);
Assert.Contains(HumidityControlMode.Dehumidify, supported);
}

[TestMethod]
public void SupportedReport_Parse_TooShort_Throws()
{
byte[] data = [0x6D, 0x05];
CommandClassFrame frame = new(data);

Assert.Throws<ZWaveException>(
() => HumidityControlModeCommandClass.HumidityControlModeSupportedReportCommand.Parse(frame, NullLogger.Instance));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ZWave.CommandClasses.Tests;

[TestClass]
public partial class HumidityControlModeCommandClassTests
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Microsoft.Extensions.Logging.Abstractions;

namespace ZWave.CommandClasses.Tests;

[TestClass]
public class HumidityControlOperatingStateCommandClassTests
{
[TestMethod]
public void GetCommand_Create_HasCorrectFormat()
{
HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateGetCommand command =
HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateGetCommand.Create();

Assert.AreEqual(CommandClassId.HumidityControlOperatingState, HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateGetCommand.CommandClassId);
Assert.AreEqual((byte)HumidityControlOperatingStateCommand.Get, HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateGetCommand.CommandId);
Assert.AreEqual(2, command.Frame.Data.Length);
}

[TestMethod]
public void Report_Parse_Idle()
{
// CC=0x6E, Cmd=0x02, OperatingState=0x00 (Idle)
byte[] data = [0x6E, 0x02, 0x00];
CommandClassFrame frame = new(data);

HumidityControlOperatingState state = HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlOperatingState.Idle, state);
}

[TestMethod]
public void Report_Parse_Humidifying()
{
// CC=0x6E, Cmd=0x02, OperatingState=0x01 (Humidifying)
byte[] data = [0x6E, 0x02, 0x01];
CommandClassFrame frame = new(data);

HumidityControlOperatingState state = HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlOperatingState.Humidifying, state);
}

[TestMethod]
public void Report_Parse_Dehumidifying()
{
// CC=0x6E, Cmd=0x02, OperatingState=0x02 (Dehumidifying)
byte[] data = [0x6E, 0x02, 0x02];
CommandClassFrame frame = new(data);

HumidityControlOperatingState state = HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlOperatingState.Dehumidifying, state);
}

[TestMethod]
public void Report_Parse_ReservedBitsIgnored()
{
// Upper 4 bits are reserved and should be ignored
// CC=0x6E, Cmd=0x02, 0xF1 = reserved bits set + Humidifying
byte[] data = [0x6E, 0x02, 0xF1];
CommandClassFrame frame = new(data);

HumidityControlOperatingState state = HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateReportCommand.Parse(frame, NullLogger.Instance);

Assert.AreEqual(HumidityControlOperatingState.Humidifying, state);
}

[TestMethod]
public void Report_Parse_TooShort_Throws()
{
// CC=0x6E, Cmd=0x02, no parameters
byte[] data = [0x6E, 0x02];
CommandClassFrame frame = new(data);

Assert.Throws<ZWaveException>(
() => HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateReportCommand.Parse(frame, NullLogger.Instance));
}
}
Loading