diff --git a/src/Shared/BinaryExtensions.cs b/src/Shared/BinaryExtensions.cs index ddb0516..d7d0b14 100644 --- a/src/Shared/BinaryExtensions.cs +++ b/src/Shared/BinaryExtensions.cs @@ -19,4 +19,65 @@ internal static class BinaryExtensions public static void WriteBytesBE(this uint value, Span destination) => BinaryPrimitives.WriteUInt32BigEndian(destination, value); public static int ToInt32BE(this ReadOnlySpan bytes) => BinaryPrimitives.ReadInt32BigEndian(bytes); + + public static void WriteBytesBE(this int value, Span destination) => BinaryPrimitives.WriteInt32BigEndian(destination, value); + + /// + /// Lookup table for 10^n where n is a Z-Wave precision value (0–7). + /// + public static ReadOnlySpan PowersOfTen => [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000]; + + /// + /// Read a signed big-endian integer from a span of 1, 2, or 4 bytes. + /// + public static int ReadSignedVariableSizeBE(this ReadOnlySpan 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."), + }; + + /// + /// Get the minimum number of bytes (1, 2, or 4) needed to represent a signed integer. + /// + public static int GetSignedVariableSize(this int value) + => value switch + { + >= sbyte.MinValue and <= sbyte.MaxValue => 1, + >= short.MinValue and <= short.MaxValue => 2, + _ => 4, + }; + + /// + /// Write a signed big-endian integer using 1, 2, or 4 bytes based on the destination length. + /// + public static void WriteSignedVariableSizeBE(this int value, Span 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)); + } + } + } } diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.Report.cs b/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.Report.cs new file mode 100644 index 0000000..bb1c24b --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.Report.cs @@ -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( + () => HumidityControlModeCommandClass.HumidityControlModeReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.Supported.cs b/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.Supported.cs new file mode 100644 index 0000000..9480000 --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.Supported.cs @@ -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 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 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 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 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 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( + () => HumidityControlModeCommandClass.HumidityControlModeSupportedReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.cs b/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.cs new file mode 100644 index 0000000..a86c06c --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlModeCommandClassTests.cs @@ -0,0 +1,6 @@ +namespace ZWave.CommandClasses.Tests; + +[TestClass] +public partial class HumidityControlModeCommandClassTests +{ +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlOperatingStateCommandClassTests.cs b/src/ZWave.CommandClasses.Tests/HumidityControlOperatingStateCommandClassTests.cs new file mode 100644 index 0000000..a7b9633 --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlOperatingStateCommandClassTests.cs @@ -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( + () => HumidityControlOperatingStateCommandClass.HumidityControlOperatingStateReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Capabilities.cs b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Capabilities.cs new file mode 100644 index 0000000..dbba480 --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Capabilities.cs @@ -0,0 +1,160 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace ZWave.CommandClasses.Tests; + +public partial class HumidityControlSetpointCommandClassTests +{ + [TestMethod] + public void CapabilitiesGetCommand_Create_HasCorrectFormat() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesGetCommand.Create(HumidityControlSetpointType.Humidifier); + + Assert.AreEqual(CommandClassId.HumidityControlSetpoint, HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesGetCommand.CommandClassId); + Assert.AreEqual((byte)HumidityControlSetpointCommand.CapabilitiesGet, HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesGetCommand.CommandId); + Assert.AreEqual(3, command.Frame.Data.Length); + Assert.AreEqual(0x01, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void CapabilitiesGetCommand_Create_Dehumidifier() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesGetCommand.Create(HumidityControlSetpointType.Dehumidifier); + + Assert.AreEqual(0x02, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void CapabilitiesReport_Parse_1ByteValues() + { + // Type=Humidifier, Min: precision=0, scale=0, size=1, value=20 + // Max: precision=0, scale=0, size=1, value=80 + // CC=0x64, Cmd=0x09, Type=0x01, MinPSS=0x01, MinVal=0x14, MaxPSS=0x01, MaxVal=0x50 + byte[] data = [0x64, 0x09, 0x01, 0x01, 0x14, 0x01, 0x50]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointCapabilities caps = HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Humidifier, caps.SetpointType); + Assert.AreEqual(HumidityControlSetpointScale.Percentage, caps.MinimumScale); + Assert.AreEqual(20.0, caps.MinimumValue); + Assert.AreEqual(HumidityControlSetpointScale.Percentage, caps.MaximumScale); + Assert.AreEqual(80.0, caps.MaximumValue); + } + + [TestMethod] + public void CapabilitiesReport_Parse_2ByteValues() + { + // Type=Dehumidifier, Min: prec=1, scale=0, size=2, value=150 (15.0) + // Max: prec=1, scale=0, size=2, value=950 (95.0) + // MinPSS = (1<<5)|(0<<3)|2 = 0x22, MinVal = 0x0096 + // MaxPSS = (1<<5)|(0<<3)|2 = 0x22, MaxVal = 0x03B6 + byte[] data = [0x64, 0x09, 0x02, 0x22, 0x00, 0x96, 0x22, 0x03, 0xB6]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointCapabilities caps = HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Dehumidifier, caps.SetpointType); + Assert.AreEqual(15.0, caps.MinimumValue, 0.001); + Assert.AreEqual(95.0, caps.MaximumValue, 0.001); + } + + [TestMethod] + public void CapabilitiesReport_Parse_DifferentScales() + { + // Min: percentage scale, Max: absolute humidity scale + // Type=Humidifier, Min: prec=0, scale=0, size=1, value=20 + // Max: prec=0, scale=1, size=1, value=50 + // MinPSS = (0<<5)|(0<<3)|1 = 0x01 + // MaxPSS = (0<<5)|(1<<3)|1 = 0x09 + byte[] data = [0x64, 0x09, 0x01, 0x01, 0x14, 0x09, 0x32]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointCapabilities caps = HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointScale.Percentage, caps.MinimumScale); + Assert.AreEqual(20.0, caps.MinimumValue); + Assert.AreEqual(HumidityControlSetpointScale.AbsoluteHumidity, caps.MaximumScale); + Assert.AreEqual(50.0, caps.MaximumValue); + } + + [TestMethod] + public void CapabilitiesReport_Parse_4ByteValues() + { + // Type=Auto, Min: prec=2, scale=0, size=4, value=1000 (10.00) + // Max: prec=2, scale=0, size=4, value=9500 (95.00) + // MinPSS = (2<<5)|(0<<3)|4 = 0x44 + // MaxPSS = (2<<5)|(0<<3)|4 = 0x44 + byte[] data = [0x64, 0x09, 0x03, + 0x44, 0x00, 0x00, 0x03, 0xE8, + 0x44, 0x00, 0x00, 0x25, 0x1C]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointCapabilities caps = HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Auto, caps.SetpointType); + Assert.AreEqual(10.0, caps.MinimumValue, 0.001); + Assert.AreEqual(95.0, caps.MaximumValue, 0.001); + } + + [TestMethod] + public void CapabilitiesReport_Parse_MixedSizes() + { + // Min uses 1 byte, Max uses 2 bytes + // Type=Humidifier, Min: prec=0, scale=0, size=1, value=10 + // Max: prec=0, scale=0, size=2, value=200 + // MinPSS = 0x01, MaxPSS = 0x02 + byte[] data = [0x64, 0x09, 0x01, 0x01, 0x0A, 0x02, 0x00, 0xC8]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointCapabilities caps = HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(10.0, caps.MinimumValue); + Assert.AreEqual(200.0, caps.MaximumValue); + } + + [TestMethod] + public void CapabilitiesReport_Parse_TooShort_Throws() + { + // Need at least type + min PSS + min value + max PSS + max value + byte[] data = [0x64, 0x09, 0x01]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance)); + } + + [TestMethod] + public void CapabilitiesReport_Parse_TooShort_MinValueMissing_Throws() + { + // Type + MinPSS(size=1) but no min value byte + byte[] data = [0x64, 0x09, 0x01, 0x01]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance)); + } + + [TestMethod] + public void CapabilitiesReport_Parse_TooShort_MaxValueMissing_Throws() + { + // Type + MinPSS + MinVal, but no max PSS or value + byte[] data = [0x64, 0x09, 0x01, 0x01, 0x14]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance)); + } + + [TestMethod] + public void CapabilitiesReport_Parse_TooShort_MaxValueTruncated_Throws() + { + // Type + MinPSS + MinVal + MaxPSS(size=2) but only 1 max value byte + byte[] data = [0x64, 0x09, 0x01, 0x01, 0x14, 0x02, 0x00]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointCapabilitiesReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Report.cs b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Report.cs new file mode 100644 index 0000000..c0d3d96 --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Report.cs @@ -0,0 +1,228 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace ZWave.CommandClasses.Tests; + +public partial class HumidityControlSetpointCommandClassTests +{ + [TestMethod] + public void GetCommand_Create_Humidifier() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand.Create(HumidityControlSetpointType.Humidifier); + + Assert.AreEqual(CommandClassId.HumidityControlSetpoint, HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand.CommandClassId); + Assert.AreEqual((byte)HumidityControlSetpointCommand.Get, HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand.CommandId); + Assert.AreEqual(3, command.Frame.Data.Length); + Assert.AreEqual(0x01, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void GetCommand_Create_Dehumidifier() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand.Create(HumidityControlSetpointType.Dehumidifier); + + Assert.AreEqual(0x02, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void GetCommand_Create_Auto() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand.Create(HumidityControlSetpointType.Auto); + + Assert.AreEqual(0x03, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void GetCommand_Create_ReservedBitsClear() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointGetCommand.Create(HumidityControlSetpointType.Auto); + + Assert.AreEqual(0x00, command.Frame.CommandParameters.Span[0] & 0xF0); + } + + [TestMethod] + public void SetCommand_Create_Percentage_1ByteValue() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand.Create( + HumidityControlSetpointType.Humidifier, + HumidityControlSetpointScale.Percentage, + 50.0); + + ReadOnlySpan parameters = command.Frame.CommandParameters.Span; + Assert.AreEqual(CommandClassId.HumidityControlSetpoint, HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand.CommandClassId); + Assert.AreEqual((byte)HumidityControlSetpointCommand.Set, HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand.CommandId); + + // Type byte + Assert.AreEqual(0x01, parameters[0] & 0x0F); + // PSS byte: precision=0, scale=0 (percentage), size=1 + Assert.AreEqual(0x01, parameters[1]); + // Value: 50 = 0x32 + Assert.AreEqual(0x32, parameters[2]); + } + + [TestMethod] + public void SetCommand_Create_AbsoluteHumidity() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand.Create( + HumidityControlSetpointType.Dehumidifier, + HumidityControlSetpointScale.AbsoluteHumidity, + 10.0); + + ReadOnlySpan parameters = command.Frame.CommandParameters.Span; + // Type byte: Dehumidifier = 0x02 + Assert.AreEqual(0x02, parameters[0] & 0x0F); + // PSS byte: precision=0, scale=1 (absolute humidity), size=1 + Assert.AreEqual(0x09, parameters[1]); // 0b00_001_001 + } + + [TestMethod] + public void SetCommand_Create_DecimalValue() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointSetCommand.Create( + HumidityControlSetpointType.Humidifier, + HumidityControlSetpointScale.Percentage, + 45.5); + + ReadOnlySpan parameters = command.Frame.CommandParameters.Span; + // Precision should be 1 (one decimal), value = 455 + int precision = (parameters[1] & 0b1110_0000) >> 5; + int valueSize = parameters[1] & 0b0000_0111; + Assert.AreEqual(1, precision); + + // 455 fits in 2 bytes (signed) + Assert.AreEqual(2, valueSize); + short rawValue = (short)((parameters[2] << 8) | parameters[3]); + Assert.AreEqual(455, rawValue); + } + + [TestMethod] + public void Report_Parse_Percentage_1Byte() + { + // CC=0x64, Cmd=0x03, Type=0x01 (Humidifier), PSS=0x01 (prec=0, scale=0, size=1), Value=0x32 (50) + byte[] data = [0x64, 0x03, 0x01, 0x01, 0x32]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointReport report = HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Humidifier, report.SetpointType); + Assert.AreEqual(HumidityControlSetpointScale.Percentage, report.Scale); + Assert.AreEqual(50.0, report.Value); + } + + [TestMethod] + public void Report_Parse_AbsoluteHumidity_1Byte() + { + // CC=0x64, Cmd=0x03, Type=0x02 (Dehumidifier), PSS=0x09 (prec=0, scale=1, size=1), Value=0x0A (10) + byte[] data = [0x64, 0x03, 0x02, 0x09, 0x0A]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointReport report = HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Dehumidifier, report.SetpointType); + Assert.AreEqual(HumidityControlSetpointScale.AbsoluteHumidity, report.Scale); + Assert.AreEqual(10.0, report.Value); + } + + [TestMethod] + public void Report_Parse_Precision2_2ByteValue() + { + // Value = 45.50 => raw = 4550 = 0x11C6, precision=2, scale=0, size=2 + // PSS = (2<<5) | (0<<3) | 2 = 0x42 + byte[] data = [0x64, 0x03, 0x01, 0x42, 0x11, 0xC6]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointReport report = HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Humidifier, report.SetpointType); + Assert.AreEqual(HumidityControlSetpointScale.Percentage, report.Scale); + Assert.AreEqual(45.50, report.Value, 0.001); + } + + [TestMethod] + public void Report_Parse_4ByteValue() + { + // Value = 1000 => raw = 1000 = 0x000003E8, precision=0, scale=0, size=4 + // PSS = (0<<5) | (0<<3) | 4 = 0x04 + byte[] data = [0x64, 0x03, 0x03, 0x04, 0x00, 0x00, 0x03, 0xE8]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointReport report = HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Auto, report.SetpointType); + Assert.AreEqual(1000.0, report.Value); + } + + [TestMethod] + public void Report_Parse_NegativeValue() + { + // Value = -5 => raw = -5, 1-byte signed = 0xFB, precision=0, scale=0, size=1 + // PSS = (0<<5) | (0<<3) | 1 = 0x01 + byte[] data = [0x64, 0x03, 0x01, 0x01, 0xFB]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointReport report = HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(-5.0, report.Value); + } + + [TestMethod] + public void Report_Parse_ReservedBitsIgnored() + { + // Upper 4 bits of type byte are reserved + // CC=0x64, Cmd=0x03, Type=0xF1 (reserved | Humidifier), PSS=0x01, Value=0x32 + byte[] data = [0x64, 0x03, 0xF1, 0x01, 0x32]; + CommandClassFrame frame = new(data); + + HumidityControlSetpointReport report = HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(HumidityControlSetpointType.Humidifier, report.SetpointType); + } + + [TestMethod] + public void Report_Parse_TooShort_NoParameters_Throws() + { + byte[] data = [0x64, 0x03]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance)); + } + + [TestMethod] + public void Report_Parse_TooShort_OnlyType_Throws() + { + byte[] data = [0x64, 0x03, 0x01]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance)); + } + + [TestMethod] + public void Report_Parse_TooShort_ValueSizeMismatch_Throws() + { + // PSS says size=2 but only 1 value byte provided + byte[] data = [0x64, 0x03, 0x01, 0x02, 0x32]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance)); + } + + [TestMethod] + public void Report_Parse_InvalidValueSize_Throws() + { + // PSS says size=3 (invalid, only 1/2/4 allowed), provide 3 bytes + byte[] data = [0x64, 0x03, 0x01, 0x03, 0x01, 0x02, 0x03]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.ScaleSupported.cs b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.ScaleSupported.cs new file mode 100644 index 0000000..87a206d --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.ScaleSupported.cs @@ -0,0 +1,94 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace ZWave.CommandClasses.Tests; + +public partial class HumidityControlSetpointCommandClassTests +{ + [TestMethod] + public void ScaleSupportedGetCommand_Create_HasCorrectFormat() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedGetCommand.Create(HumidityControlSetpointType.Humidifier); + + Assert.AreEqual(CommandClassId.HumidityControlSetpoint, HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedGetCommand.CommandClassId); + Assert.AreEqual((byte)HumidityControlSetpointCommand.ScaleSupportedGet, HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedGetCommand.CommandId); + Assert.AreEqual(3, command.Frame.Data.Length); + Assert.AreEqual(0x01, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void ScaleSupportedReport_Parse_PercentageOnly() + { + // CC=0x64, Cmd=0x07, ScaleBitMask=0x01 (bit 0 = Percentage) + byte[] data = [0x64, 0x07, 0x01]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(1, supported); + Assert.Contains(HumidityControlSetpointScale.Percentage, supported); + } + + [TestMethod] + public void ScaleSupportedReport_Parse_BothScales() + { + // CC=0x64, Cmd=0x07, ScaleBitMask=0x03 (bits 0,1 = Percentage, AbsoluteHumidity) + byte[] data = [0x64, 0x07, 0x03]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(2, supported); + Assert.Contains(HumidityControlSetpointScale.Percentage, supported); + Assert.Contains(HumidityControlSetpointScale.AbsoluteHumidity, supported); + } + + [TestMethod] + public void ScaleSupportedReport_Parse_AbsoluteHumidityOnly() + { + // CC=0x64, Cmd=0x07, ScaleBitMask=0x02 (bit 1 = AbsoluteHumidity) + byte[] data = [0x64, 0x07, 0x02]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(1, supported); + Assert.Contains(HumidityControlSetpointScale.AbsoluteHumidity, supported); + } + + [TestMethod] + public void ScaleSupportedReport_Parse_ReservedUpperBitsIgnored() + { + // Upper 4 bits are reserved; only lower 4 bits matter + // CC=0x64, Cmd=0x07, 0xF3 = reserved upper bits | both scales + byte[] data = [0x64, 0x07, 0xF3]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(2, supported); + Assert.Contains(HumidityControlSetpointScale.Percentage, supported); + Assert.Contains(HumidityControlSetpointScale.AbsoluteHumidity, supported); + } + + [TestMethod] + public void ScaleSupportedReport_Parse_EmptyMask() + { + byte[] data = [0x64, 0x07, 0x00]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.IsEmpty(supported); + } + + [TestMethod] + public void ScaleSupportedReport_Parse_TooShort_Throws() + { + byte[] data = [0x64, 0x07]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointScaleSupportedReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Supported.cs b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Supported.cs new file mode 100644 index 0000000..9df8032 --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.Supported.cs @@ -0,0 +1,96 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace ZWave.CommandClasses.Tests; + +public partial class HumidityControlSetpointCommandClassTests +{ + [TestMethod] + public void SupportedGetCommand_Create_HasCorrectFormat() + { + HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedGetCommand command = + HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedGetCommand.Create(); + + Assert.AreEqual(CommandClassId.HumidityControlSetpoint, HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedGetCommand.CommandClassId); + Assert.AreEqual((byte)HumidityControlSetpointCommand.SupportedGet, HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedGetCommand.CommandId); + Assert.AreEqual(2, command.Frame.Data.Length); + } + + [TestMethod] + public void SupportedReport_Parse_HumidifierAndDehumidifier() + { + // CC=0x64, Cmd=0x05, BitMask=0x06 (bits 1,2 = Humidifier, Dehumidifier) + byte[] data = [0x64, 0x05, 0x06]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(2, supported); + Assert.Contains(HumidityControlSetpointType.Humidifier, supported); + Assert.Contains(HumidityControlSetpointType.Dehumidifier, supported); + } + + [TestMethod] + public void SupportedReport_Parse_AllTypes() + { + // CC=0x64, Cmd=0x05, BitMask=0x0E (bits 1,2,3 = Humidifier, Dehumidifier, Auto) + byte[] data = [0x64, 0x05, 0x0E]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(3, supported); + Assert.Contains(HumidityControlSetpointType.Humidifier, supported); + Assert.Contains(HumidityControlSetpointType.Dehumidifier, supported); + Assert.Contains(HumidityControlSetpointType.Auto, supported); + } + + [TestMethod] + public void SupportedReport_Parse_ReservedBit0Ignored() + { + // Bit 0 is reserved per spec. If a device sets it, it should be ignored. + // CC=0x64, Cmd=0x05, BitMask=0x07 (bits 0,1,2) + byte[] data = [0x64, 0x05, 0x07]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedReportCommand.Parse(frame, NullLogger.Instance); + + // Bit 0 should not produce a result since it's reserved (type 0 is invalid) + Assert.HasCount(2, supported); + Assert.Contains(HumidityControlSetpointType.Humidifier, supported); + Assert.Contains(HumidityControlSetpointType.Dehumidifier, supported); + } + + [TestMethod] + public void SupportedReport_Parse_EmptyMask() + { + byte[] data = [0x64, 0x05, 0x00]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.IsEmpty(supported); + } + + [TestMethod] + public void SupportedReport_Parse_MultipleMaskBytes() + { + // CC=0x64, Cmd=0x05, Mask1=0x02, Mask2=0x00 + byte[] data = [0x64, 0x05, 0x02, 0x00]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(1, supported); + Assert.Contains(HumidityControlSetpointType.Humidifier, supported); + } + + [TestMethod] + public void SupportedReport_Parse_TooShort_Throws() + { + byte[] data = [0x64, 0x05]; + CommandClassFrame frame = new(data); + + Assert.Throws( + () => HumidityControlSetpointCommandClass.HumidityControlSetpointSupportedReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.cs b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.cs new file mode 100644 index 0000000..94b76e4 --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/HumidityControlSetpointCommandClassTests.cs @@ -0,0 +1,6 @@ +namespace ZWave.CommandClasses.Tests; + +[TestClass] +public partial class HumidityControlSetpointCommandClassTests +{ +} diff --git a/src/ZWave.CommandClasses/BarrierOperatorCommandClass.SignalingCapabilities.cs b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.SignalingCapabilities.cs index 60ed584..cde1a87 100644 --- a/src/ZWave.CommandClasses/BarrierOperatorCommandClass.SignalingCapabilities.cs +++ b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.SignalingCapabilities.cs @@ -73,24 +73,7 @@ public static IReadOnlySet Parse(CommandC "Barrier Operator Signal Supported Report frame is too short"); } - HashSet supported = new HashSet(); - - ReadOnlySpan bitMask = frame.CommandParameters.Span; - for (int byteNum = 0; byteNum < bitMask.Length; byteNum++) - { - for (int bitNum = 0; bitNum < 8; bitNum++) - { - if ((bitMask[byteNum] & (1 << bitNum)) != 0) - { - // Per spec: bit 0 = subsystem type 0x01, bit 1 = type 0x02, etc. - BarrierOperatorSignalingSubsystemType subsystemType = - (BarrierOperatorSignalingSubsystemType)((byteNum << 3) + bitNum + 1); - supported.Add(subsystemType); - } - } - } - - return supported; + return BitMaskHelper.ParseBitMask(frame.CommandParameters.Span, offset: 1); } } } diff --git a/src/ZWave.CommandClasses/BatteryCommandClass.Health.cs b/src/ZWave.CommandClasses/BatteryCommandClass.Health.cs index 1384c1b..0df8adc 100644 --- a/src/ZWave.CommandClasses/BatteryCommandClass.Health.cs +++ b/src/ZWave.CommandClasses/BatteryCommandClass.Health.cs @@ -1,4 +1,3 @@ -using System.Buffers.Binary; using Microsoft.Extensions.Logging; namespace ZWave.CommandClasses; @@ -125,14 +124,8 @@ BatteryTemperatureScale batteryTemperatureScale ReadOnlySpan valueBytes = frame.CommandParameters.Span.Slice(2, valueSize); // CC:0080.02.05.11.010: signed big-endian encoding - int rawValue = valueSize switch - { - 1 => (sbyte)valueBytes[0], - 2 => BinaryPrimitives.ReadInt16BigEndian(valueBytes), - 4 => BinaryPrimitives.ReadInt32BigEndian(valueBytes), - _ => throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Unexpected value size"), - }; - batteryTemperature = rawValue / Math.Pow(10, precision); + int rawValue = valueBytes.ReadSignedVariableSizeBE(); + batteryTemperature = rawValue / BinaryExtensions.PowersOfTen[precision]; } return new BatteryHealth(maximumCapacity, batteryTemperatureScale, batteryTemperature); diff --git a/src/ZWave.CommandClasses/BinarySensorCommandClass.SupportedSensor.cs b/src/ZWave.CommandClasses/BinarySensorCommandClass.SupportedSensor.cs index 173fe69..576decb 100644 --- a/src/ZWave.CommandClasses/BinarySensorCommandClass.SupportedSensor.cs +++ b/src/ZWave.CommandClasses/BinarySensorCommandClass.SupportedSensor.cs @@ -78,22 +78,7 @@ public static IReadOnlySet Parse(CommandClassFrame frame, ILog throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Binary Sensor Supported Report frame is too short"); } - HashSet supportedSensorTypes = new HashSet(); - - ReadOnlySpan bitMask = frame.CommandParameters.Span; - for (int byteNum = 0; byteNum < bitMask.Length; byteNum++) - { - for (int bitNum = 0; bitNum < 8; bitNum++) - { - if ((bitMask[byteNum] & (1 << bitNum)) != 0) - { - BinarySensorType sensorType = (BinarySensorType)((byteNum << 3) + bitNum); - supportedSensorTypes.Add(sensorType); - } - } - } - - return supportedSensorTypes; + return BitMaskHelper.ParseBitMask(frame.CommandParameters.Span); } } } diff --git a/src/ZWave.CommandClasses/BitMaskHelper.cs b/src/ZWave.CommandClasses/BitMaskHelper.cs new file mode 100644 index 0000000..8f22c74 --- /dev/null +++ b/src/ZWave.CommandClasses/BitMaskHelper.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace ZWave.CommandClasses; + +/// +/// Helpers for parsing Z-Wave bitmask fields into collections of enum values. +/// +internal static class BitMaskHelper +{ + /// + /// Parse a variable-length bitmask into a set of enum values. + /// + /// A byte-backed enum type. + /// The bitmask bytes to parse. + /// Value added to each bit position to produce the enum value. Default 0. + /// First bit position to consider; earlier bits are skipped. Default 0. + /// A set containing the enum values corresponding to set bits. + internal static HashSet ParseBitMask( + ReadOnlySpan bitMask, + int offset = 0, + int startBit = 0) + where TEnum : struct, Enum + { + Debug.Assert(Unsafe.SizeOf() == sizeof(byte), "ParseBitMask only supports byte-backed enums."); + + HashSet result = []; + for (int byteNum = 0; byteNum < bitMask.Length; byteNum++) + { + for (int bitNum = 0; bitNum < 8; bitNum++) + { + if ((bitMask[byteNum] & (1 << bitNum)) != 0) + { + int bitPosition = (byteNum << 3) + bitNum; + if (bitPosition >= startBit) + { + byte value = (byte)(bitPosition + offset); + result.Add(Unsafe.BitCast(value)); + } + } + } + } + + return result; + } +} diff --git a/src/ZWave.CommandClasses/ColorSwitchCommandClass.Supported.cs b/src/ZWave.CommandClasses/ColorSwitchCommandClass.Supported.cs index aac8890..bc4895d 100644 --- a/src/ZWave.CommandClasses/ColorSwitchCommandClass.Supported.cs +++ b/src/ZWave.CommandClasses/ColorSwitchCommandClass.Supported.cs @@ -85,22 +85,7 @@ public static IReadOnlySet Parse(CommandClassFrame fr throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Color Switch Supported Report frame is too short"); } - HashSet supportedComponents = []; - - ReadOnlySpan bitMask = frame.CommandParameters.Span; - for (int byteNum = 0; byteNum < bitMask.Length; byteNum++) - { - for (int bitNum = 0; bitNum < 8; bitNum++) - { - if ((bitMask[byteNum] & (1 << bitNum)) != 0) - { - ColorSwitchColorComponent colorComponent = (ColorSwitchColorComponent)((byteNum << 3) + bitNum); - supportedComponents.Add(colorComponent); - } - } - } - - return supportedComponents; + return BitMaskHelper.ParseBitMask(frame.CommandParameters.Span); } } } diff --git a/src/ZWave.CommandClasses/EntryControlCommandClass.EventSupported.cs b/src/ZWave.CommandClasses/EntryControlCommandClass.EventSupported.cs index af4b73e..2da38de 100644 --- a/src/ZWave.CommandClasses/EntryControlCommandClass.EventSupported.cs +++ b/src/ZWave.CommandClasses/EntryControlCommandClass.EventSupported.cs @@ -121,18 +121,7 @@ public static EntryControlEventSupportedReport Parse(CommandClassFrame frame, IL } // Parse data type supported bitmask - HashSet supportedDataTypes = []; - for (int byteNum = 0; byteNum < dataTypeBitmaskLength; byteNum++) - { - for (int bitNum = 0; bitNum < 8; bitNum++) - { - if ((span[offset + byteNum] & (1 << bitNum)) != 0) - { - EntryControlDataType dataType = (EntryControlDataType)((byteNum << 3) + bitNum); - supportedDataTypes.Add(dataType); - } - } - } + HashSet supportedDataTypes = BitMaskHelper.ParseBitMask(span.Slice(offset, dataTypeBitmaskLength)); offset += dataTypeBitmaskLength; @@ -163,18 +152,7 @@ public static EntryControlEventSupportedReport Parse(CommandClassFrame frame, IL } // Parse event type supported bitmask - HashSet supportedEventTypes = []; - for (int byteNum = 0; byteNum < eventTypeBitmaskLength; byteNum++) - { - for (int bitNum = 0; bitNum < 8; bitNum++) - { - if ((span[offset + byteNum] & (1 << bitNum)) != 0) - { - EntryControlEventType eventTypeValue = (EntryControlEventType)((byteNum << 3) + bitNum); - supportedEventTypes.Add(eventTypeValue); - } - } - } + HashSet supportedEventTypes = BitMaskHelper.ParseBitMask(span.Slice(offset, eventTypeBitmaskLength)); offset += eventTypeBitmaskLength; diff --git a/src/ZWave.CommandClasses/HumidityControlModeCommandClass.Report.cs b/src/ZWave.CommandClasses/HumidityControlModeCommandClass.Report.cs new file mode 100644 index 0000000..9daf6e3 --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlModeCommandClass.Report.cs @@ -0,0 +1,116 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// Represents a Humidity Control Mode Report received from a device. +/// +public readonly record struct HumidityControlModeReport( + /// + /// The current humidity control mode. + /// + HumidityControlMode Mode); + +public sealed partial class HumidityControlModeCommandClass +{ + /// + /// Gets the last report received from the device. + /// + public HumidityControlModeReport? LastReport { get; private set; } + + /// + /// Event raised when a Humidity Control Mode Report is received, both solicited and unsolicited. + /// + public event Action? OnModeReportReceived; + + /// + /// Request the current humidity control mode from the device. + /// + public async Task GetAsync(CancellationToken cancellationToken) + { + HumidityControlModeGetCommand command = HumidityControlModeGetCommand.Create(); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync(cancellationToken).ConfigureAwait(false); + HumidityControlModeReport report = HumidityControlModeReportCommand.Parse(reportFrame, Logger); + LastReport = report; + OnModeReportReceived?.Invoke(report); + return report; + } + + /// + /// Set the humidity control mode in the device. + /// + public async Task SetAsync(HumidityControlMode mode, CancellationToken cancellationToken) + { + var command = HumidityControlModeSetCommand.Create(mode); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + } + + internal readonly struct HumidityControlModeSetCommand : ICommand + { + public HumidityControlModeSetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlMode; + + public static byte CommandId => (byte)HumidityControlModeCommand.Set; + + public CommandClassFrame Frame { get; } + + public static HumidityControlModeSetCommand Create(HumidityControlMode mode) + { + ReadOnlySpan commandParameters = [(byte)((byte)mode & 0x0F)]; + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new HumidityControlModeSetCommand(frame); + } + } + + internal readonly struct HumidityControlModeGetCommand : ICommand + { + public HumidityControlModeGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlMode; + + public static byte CommandId => (byte)HumidityControlModeCommand.Get; + + public CommandClassFrame Frame { get; } + + public static HumidityControlModeGetCommand Create() + { + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId); + return new HumidityControlModeGetCommand(frame); + } + } + + internal readonly struct HumidityControlModeReportCommand : ICommand + { + public HumidityControlModeReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlMode; + + public static byte CommandId => (byte)HumidityControlModeCommand.Report; + + public CommandClassFrame Frame { get; } + + public static HumidityControlModeReport Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 1) + { + logger.LogWarning("Humidity Control Mode Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Mode Report frame is too short"); + } + + ReadOnlySpan span = frame.CommandParameters.Span; + HumidityControlMode mode = (HumidityControlMode)(span[0] & 0x0F); + return new HumidityControlModeReport(mode); + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlModeCommandClass.Supported.cs b/src/ZWave.CommandClasses/HumidityControlModeCommandClass.Supported.cs new file mode 100644 index 0000000..8661802 --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlModeCommandClass.Supported.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +public sealed partial class HumidityControlModeCommandClass +{ + /// + /// Gets the humidity control modes supported by the device, or if not yet known. + /// + public IReadOnlySet? SupportedModes { get; private set; } + + /// + /// Request the supported humidity control modes from the device. + /// + public async Task> GetSupportedModesAsync(CancellationToken cancellationToken) + { + HumidityControlModeSupportedGetCommand command = HumidityControlModeSupportedGetCommand.Create(); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync(cancellationToken).ConfigureAwait(false); + IReadOnlySet supportedModes = HumidityControlModeSupportedReportCommand.Parse(reportFrame, Logger); + SupportedModes = supportedModes; + return supportedModes; + } + + internal readonly struct HumidityControlModeSupportedGetCommand : ICommand + { + public HumidityControlModeSupportedGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlMode; + + public static byte CommandId => (byte)HumidityControlModeCommand.SupportedGet; + + public CommandClassFrame Frame { get; } + + public static HumidityControlModeSupportedGetCommand Create() + { + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId); + return new HumidityControlModeSupportedGetCommand(frame); + } + } + + internal readonly struct HumidityControlModeSupportedReportCommand : ICommand + { + public HumidityControlModeSupportedReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlMode; + + public static byte CommandId => (byte)HumidityControlModeCommand.SupportedReport; + + public CommandClassFrame Frame { get; } + + public static IReadOnlySet Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 1) + { + logger.LogWarning("Humidity Control Mode Supported Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Mode Supported Report frame is too short"); + } + + return BitMaskHelper.ParseBitMask(frame.CommandParameters.Span, startBit: 1); + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlModeCommandClass.cs b/src/ZWave.CommandClasses/HumidityControlModeCommandClass.cs new file mode 100644 index 0000000..78b032f --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlModeCommandClass.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// The humidity control mode. +/// +public enum HumidityControlMode : byte +{ + /// + /// The humidity control system is off. + /// + Off = 0x00, + + /// + /// The system will attempt to raise humidity to the humidifier setpoint. + /// + Humidify = 0x01, + + /// + /// The system will attempt to lower humidity to the de-humidifier setpoint. + /// + Dehumidify = 0x02, + + /// + /// The system will automatically switch between humidifying and de-humidifying + /// in order to satisfy the humidify and de-humidify setpoints. + /// + Auto = 0x03, +} + +/// +/// Commands for the Humidity Control Mode Command Class. +/// +public enum HumidityControlModeCommand : byte +{ + /// + /// Set the humidity control mode in the device. + /// + Set = 0x01, + + /// + /// Request the current humidity control mode from the device. + /// + Get = 0x02, + + /// + /// Report the current humidity control mode from the device. + /// + Report = 0x03, + + /// + /// Request the supported humidity control modes from the device. + /// + SupportedGet = 0x04, + + /// + /// Report the supported humidity control modes from the device. + /// + SupportedReport = 0x05, +} + +/// +/// Implements the Humidity Control Mode Command Class (V1-2). +/// +[CommandClass(CommandClassId.HumidityControlMode)] +public sealed partial class HumidityControlModeCommandClass : CommandClass +{ + internal HumidityControlModeCommandClass( + CommandClassInfo info, + IDriver driver, + IEndpoint endpoint, + ILogger logger) + : base(info, driver, endpoint, logger) + { + } + + /// + public override bool? IsCommandSupported(HumidityControlModeCommand command) + => command switch + { + HumidityControlModeCommand.Set => true, + HumidityControlModeCommand.Get => true, + HumidityControlModeCommand.SupportedGet => true, + _ => false, + }; + + /// + internal override async Task InterviewAsync(CancellationToken cancellationToken) + { + _ = await GetSupportedModesAsync(cancellationToken).ConfigureAwait(false); + _ = await GetAsync(cancellationToken).ConfigureAwait(false); + } + + /// + protected override void ProcessUnsolicitedCommand(CommandClassFrame frame) + { + switch ((HumidityControlModeCommand)frame.CommandId) + { + case HumidityControlModeCommand.Report: + { + HumidityControlModeReport report = HumidityControlModeReportCommand.Parse(frame, Logger); + LastReport = report; + OnModeReportReceived?.Invoke(report); + break; + } + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlOperatingStateCommandClass.cs b/src/ZWave.CommandClasses/HumidityControlOperatingStateCommandClass.cs new file mode 100644 index 0000000..e357f78 --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlOperatingStateCommandClass.cs @@ -0,0 +1,154 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// The operating state of a humidity control device. +/// +public enum HumidityControlOperatingState : byte +{ + /// + /// The humidity control system is idle. + /// + Idle = 0x00, + + /// + /// The system is humidifying. + /// + Humidifying = 0x01, + + /// + /// The system is de-humidifying. + /// + Dehumidifying = 0x02, +} + +/// +/// Commands for the Humidity Control Operating State Command Class. +/// +public enum HumidityControlOperatingStateCommand : byte +{ + /// + /// Request the operating state of the humidity control device. + /// + Get = 0x01, + + /// + /// Report the operating state of the humidity control device. + /// + Report = 0x02, +} + +/// +/// Implements the Humidity Control Operating State Command Class (V1). +/// +[CommandClass(CommandClassId.HumidityControlOperatingState)] +public sealed class HumidityControlOperatingStateCommandClass : CommandClass +{ + internal HumidityControlOperatingStateCommandClass( + CommandClassInfo info, + IDriver driver, + IEndpoint endpoint, + ILogger logger) + : base(info, driver, endpoint, logger) + { + } + + /// + /// Gets the last operating state received from the device. + /// + public HumidityControlOperatingState? OperatingState { get; private set; } + + /// + /// Event raised when a Humidity Control Operating State Report is received, both solicited and unsolicited. + /// + public event Action? OnOperatingStateReportReceived; + + /// + public override bool? IsCommandSupported(HumidityControlOperatingStateCommand command) + => command switch + { + HumidityControlOperatingStateCommand.Get => true, + _ => false, + }; + + /// + internal override async Task InterviewAsync(CancellationToken cancellationToken) + { + _ = await GetAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Request the operating state of the humidity control device. + /// + public async Task GetAsync(CancellationToken cancellationToken) + { + HumidityControlOperatingStateGetCommand command = HumidityControlOperatingStateGetCommand.Create(); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync(cancellationToken).ConfigureAwait(false); + HumidityControlOperatingState state = HumidityControlOperatingStateReportCommand.Parse(reportFrame, Logger); + OperatingState = state; + OnOperatingStateReportReceived?.Invoke(state); + return state; + } + + /// + protected override void ProcessUnsolicitedCommand(CommandClassFrame frame) + { + switch ((HumidityControlOperatingStateCommand)frame.CommandId) + { + case HumidityControlOperatingStateCommand.Report: + { + HumidityControlOperatingState state = HumidityControlOperatingStateReportCommand.Parse(frame, Logger); + OperatingState = state; + OnOperatingStateReportReceived?.Invoke(state); + break; + } + } + } + + internal readonly struct HumidityControlOperatingStateGetCommand : ICommand + { + public HumidityControlOperatingStateGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlOperatingState; + + public static byte CommandId => (byte)HumidityControlOperatingStateCommand.Get; + + public CommandClassFrame Frame { get; } + + public static HumidityControlOperatingStateGetCommand Create() + { + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId); + return new HumidityControlOperatingStateGetCommand(frame); + } + } + + internal readonly struct HumidityControlOperatingStateReportCommand : ICommand + { + public HumidityControlOperatingStateReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlOperatingState; + + public static byte CommandId => (byte)HumidityControlOperatingStateCommand.Report; + + public CommandClassFrame Frame { get; } + + public static HumidityControlOperatingState Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 1) + { + logger.LogWarning("Humidity Control Operating State Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Operating State Report frame is too short"); + } + + return (HumidityControlOperatingState)(frame.CommandParameters.Span[0] & 0x0F); + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Capabilities.cs b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Capabilities.cs new file mode 100644 index 0000000..5b0e6b9 --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Capabilities.cs @@ -0,0 +1,147 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// Represents the capabilities (min/max values) for a humidity control setpoint type. +/// +public readonly record struct HumidityControlSetpointCapabilities( + /// + /// The setpoint type. + /// + HumidityControlSetpointType SetpointType, + + /// + /// The scale of the minimum value. + /// + HumidityControlSetpointScale MinimumScale, + + /// + /// The minimum setpoint value. + /// + double MinimumValue, + + /// + /// The scale of the maximum value. + /// + HumidityControlSetpointScale MaximumScale, + + /// + /// The maximum setpoint value. + /// + double MaximumValue); + +public sealed partial class HumidityControlSetpointCommandClass +{ + private Dictionary? _capabilities; + + /// + /// Gets the capabilities (min/max) per setpoint type, or if not yet known. + /// + public IReadOnlyDictionary? Capabilities => _capabilities; + + /// + /// Request the minimum and maximum setpoint values for a given setpoint type. + /// + public async Task GetCapabilitiesAsync( + HumidityControlSetpointType setpointType, + CancellationToken cancellationToken) + { + var command = HumidityControlSetpointCapabilitiesGetCommand.Create(setpointType); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync( + predicate: frame => + { + return frame.CommandParameters.Length > 0 + && (HumidityControlSetpointType)(frame.CommandParameters.Span[0] & 0x0F) == setpointType; + }, + cancellationToken).ConfigureAwait(false); + HumidityControlSetpointCapabilities capabilities = HumidityControlSetpointCapabilitiesReportCommand.Parse(reportFrame, Logger); + + _capabilities ??= []; + _capabilities[setpointType] = capabilities; + + return capabilities; + } + + internal readonly struct HumidityControlSetpointCapabilitiesGetCommand : ICommand + { + public HumidityControlSetpointCapabilitiesGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.CapabilitiesGet; + + public CommandClassFrame Frame { get; } + + public static HumidityControlSetpointCapabilitiesGetCommand Create(HumidityControlSetpointType setpointType) + { + ReadOnlySpan commandParameters = [(byte)((byte)setpointType & 0x0F)]; + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new HumidityControlSetpointCapabilitiesGetCommand(frame); + } + } + + internal readonly struct HumidityControlSetpointCapabilitiesReportCommand : ICommand + { + public HumidityControlSetpointCapabilitiesReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.CapabilitiesReport; + + public CommandClassFrame Frame { get; } + + public static HumidityControlSetpointCapabilities Parse(CommandClassFrame frame, ILogger logger) + { + // Minimum payload: type(1) + min PSS(1) + at least 1 min value byte + max PSS(1) + at least 1 max value byte = 5 + if (frame.CommandParameters.Length < 3) + { + logger.LogWarning("Humidity Control Setpoint Capabilities Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Setpoint Capabilities Report frame is too short"); + } + + ReadOnlySpan span = frame.CommandParameters.Span; + HumidityControlSetpointType setpointType = (HumidityControlSetpointType)(span[0] & 0x0F); + + // Parse minimum value + (int minPrecision, HumidityControlSetpointScale minScale, int minValueSize) = ParsePrecisionScaleSize(span[1]); + + if (frame.CommandParameters.Length < 2 + minValueSize + 1) + { + logger.LogWarning( + "Humidity Control Setpoint Capabilities Report frame is too short for minimum value ({Length} bytes, need {Needed})", + frame.CommandParameters.Length, + 2 + minValueSize + 1); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Setpoint Capabilities Report frame is too short for minimum value"); + } + + ReadOnlySpan minValueBytes = span.Slice(2, minValueSize); + double minValue = ParseValue(minValueBytes, minPrecision); + + // Parse maximum value + int maxPssOffset = 2 + minValueSize; + (int maxPrecision, HumidityControlSetpointScale maxScale, int maxValueSize) = ParsePrecisionScaleSize(span[maxPssOffset]); + + if (frame.CommandParameters.Length < maxPssOffset + 1 + maxValueSize) + { + logger.LogWarning( + "Humidity Control Setpoint Capabilities Report frame is too short for maximum value ({Length} bytes, need {Needed})", + frame.CommandParameters.Length, + maxPssOffset + 1 + maxValueSize); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Setpoint Capabilities Report frame is too short for maximum value"); + } + + ReadOnlySpan maxValueBytes = span.Slice(maxPssOffset + 1, maxValueSize); + double maxValue = ParseValue(maxValueBytes, maxPrecision); + + return new HumidityControlSetpointCapabilities(setpointType, minScale, minValue, maxScale, maxValue); + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Report.cs b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Report.cs new file mode 100644 index 0000000..1ef2a6e --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Report.cs @@ -0,0 +1,164 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// Represents a Humidity Control Setpoint Report received from a device. +/// +public readonly record struct HumidityControlSetpointReport( + /// + /// The setpoint type. + /// + HumidityControlSetpointType SetpointType, + + /// + /// The scale of the setpoint value. + /// + HumidityControlSetpointScale Scale, + + /// + /// The setpoint value. + /// + double Value); + +public sealed partial class HumidityControlSetpointCommandClass +{ + private Dictionary _setpointValues = new(); + + /// + /// Gets the latest setpoint values per setpoint type. + /// + public IReadOnlyDictionary SetpointValues => _setpointValues; + + /// + /// Event raised when a Humidity Control Setpoint Report is received, both solicited and unsolicited. + /// + public event Action? OnSetpointReportReceived; + + /// + /// Request the current setpoint value for a given setpoint type. + /// + public async Task GetAsync( + HumidityControlSetpointType setpointType, + CancellationToken cancellationToken) + { + var command = HumidityControlSetpointGetCommand.Create(setpointType); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync( + predicate: frame => + { + return frame.CommandParameters.Length > 0 + && (HumidityControlSetpointType)(frame.CommandParameters.Span[0] & 0x0F) == setpointType; + }, + cancellationToken).ConfigureAwait(false); + HumidityControlSetpointReport report = HumidityControlSetpointReportCommand.Parse(reportFrame, Logger); + _setpointValues[report.SetpointType] = report; + OnSetpointReportReceived?.Invoke(report); + return report; + } + + /// + /// Set the humidity control setpoint value. + /// + public async Task SetAsync( + HumidityControlSetpointType setpointType, + HumidityControlSetpointScale scale, + double value, + CancellationToken cancellationToken) + { + var command = HumidityControlSetpointSetCommand.Create(setpointType, scale, value); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + } + + internal readonly struct HumidityControlSetpointSetCommand : ICommand + { + public HumidityControlSetpointSetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.Set; + + public CommandClassFrame Frame { get; } + + public static HumidityControlSetpointSetCommand Create( + HumidityControlSetpointType setpointType, + HumidityControlSetpointScale scale, + double value) + { + (int rawValue, int valueSize, int precision) = EncodeValue(value); + + Span commandParameters = stackalloc byte[2 + valueSize]; + commandParameters[0] = (byte)((byte)setpointType & 0x0F); + commandParameters[1] = EncodePrecisionScaleSize(precision, scale, valueSize); + rawValue.WriteSignedVariableSizeBE(commandParameters.Slice(2, valueSize)); + + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new HumidityControlSetpointSetCommand(frame); + } + } + + internal readonly struct HumidityControlSetpointGetCommand : ICommand + { + public HumidityControlSetpointGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.Get; + + public CommandClassFrame Frame { get; } + + public static HumidityControlSetpointGetCommand Create(HumidityControlSetpointType setpointType) + { + ReadOnlySpan commandParameters = [(byte)((byte)setpointType & 0x0F)]; + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new HumidityControlSetpointGetCommand(frame); + } + } + + internal readonly struct HumidityControlSetpointReportCommand : ICommand + { + public HumidityControlSetpointReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.Report; + + public CommandClassFrame Frame { get; } + + public static HumidityControlSetpointReport Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 2) + { + logger.LogWarning("Humidity Control Setpoint Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Setpoint Report frame is too short"); + } + + ReadOnlySpan span = frame.CommandParameters.Span; + HumidityControlSetpointType setpointType = (HumidityControlSetpointType)(span[0] & 0x0F); + (int precision, HumidityControlSetpointScale scale, int valueSize) = ParsePrecisionScaleSize(span[1]); + + if (frame.CommandParameters.Length < 2 + valueSize) + { + logger.LogWarning( + "Humidity Control Setpoint Report frame value size ({ValueSize}) exceeds remaining bytes ({Remaining})", + valueSize, + frame.CommandParameters.Length - 2); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Setpoint Report frame is too short for declared value size"); + } + + ReadOnlySpan valueBytes = span.Slice(2, valueSize); + double value = ParseValue(valueBytes, precision); + + return new HumidityControlSetpointReport(setpointType, scale, value); + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.ScaleSupported.cs b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.ScaleSupported.cs new file mode 100644 index 0000000..fa32e02 --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.ScaleSupported.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +public sealed partial class HumidityControlSetpointCommandClass +{ + private Dictionary?>? _supportedScales; + + /// + /// Gets the supported scales per setpoint type, or if not yet known. + /// + public IReadOnlyDictionary?>? SupportedScales => _supportedScales; + + /// + /// Request the supported scales for a given setpoint type. + /// + public async Task> GetScaleSupportedAsync( + HumidityControlSetpointType setpointType, + CancellationToken cancellationToken) + { + var command = HumidityControlSetpointScaleSupportedGetCommand.Create(setpointType); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync(cancellationToken).ConfigureAwait(false); + IReadOnlySet supportedScales = HumidityControlSetpointScaleSupportedReportCommand.Parse(reportFrame, Logger); + + _supportedScales ??= new Dictionary?>(); + _supportedScales[setpointType] = supportedScales; + + return supportedScales; + } + + internal readonly struct HumidityControlSetpointScaleSupportedGetCommand : ICommand + { + public HumidityControlSetpointScaleSupportedGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.ScaleSupportedGet; + + public CommandClassFrame Frame { get; } + + public static HumidityControlSetpointScaleSupportedGetCommand Create(HumidityControlSetpointType setpointType) + { + ReadOnlySpan commandParameters = [(byte)((byte)setpointType & 0x0F)]; + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new HumidityControlSetpointScaleSupportedGetCommand(frame); + } + } + + internal readonly struct HumidityControlSetpointScaleSupportedReportCommand : ICommand + { + public HumidityControlSetpointScaleSupportedReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.ScaleSupportedReport; + + public CommandClassFrame Frame { get; } + + public static IReadOnlySet Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 1) + { + logger.LogWarning("Humidity Control Setpoint Scale Supported Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Setpoint Scale Supported Report frame is too short"); + } + + HashSet supportedScales = []; + + // The Scale Bit Mask is in the lower 4 bits of the first byte + byte scaleBitMask = (byte)(frame.CommandParameters.Span[0] & 0x0F); + for (int bitNum = 0; bitNum < 4; bitNum++) + { + if ((scaleBitMask & (1 << bitNum)) != 0) + { + supportedScales.Add((HumidityControlSetpointScale)bitNum); + } + } + + return supportedScales; + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Supported.cs b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Supported.cs new file mode 100644 index 0000000..f6eb8cd --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.Supported.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +public sealed partial class HumidityControlSetpointCommandClass +{ + /// + /// Gets the humidity control setpoint types supported by the device, or if not yet known. + /// + public IReadOnlySet? SupportedSetpointTypes { get; private set; } + + /// + /// Request the humidity control setpoint types supported by the device. + /// + public async Task> GetSupportedAsync(CancellationToken cancellationToken) + { + var command = HumidityControlSetpointSupportedGetCommand.Create(); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync(cancellationToken).ConfigureAwait(false); + IReadOnlySet supportedTypes = HumidityControlSetpointSupportedReportCommand.Parse(reportFrame, Logger); + + SupportedSetpointTypes = supportedTypes; + + // Rebuild the setpoint values dictionary to include keys for every supported type + Dictionary newSetpointValues = new(); + foreach (HumidityControlSetpointType st in supportedTypes) + { + if (!_setpointValues.TryGetValue(st, out HumidityControlSetpointReport? existingValue)) + { + existingValue = null; + } + + newSetpointValues.Add(st, existingValue); + } + + _setpointValues = newSetpointValues; + + return supportedTypes; + } + + internal readonly struct HumidityControlSetpointSupportedGetCommand : ICommand + { + public HumidityControlSetpointSupportedGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.SupportedGet; + + public CommandClassFrame Frame { get; } + + public static HumidityControlSetpointSupportedGetCommand Create() + { + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId); + return new HumidityControlSetpointSupportedGetCommand(frame); + } + } + + internal readonly struct HumidityControlSetpointSupportedReportCommand : ICommand + { + public HumidityControlSetpointSupportedReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.HumidityControlSetpoint; + + public static byte CommandId => (byte)HumidityControlSetpointCommand.SupportedReport; + + public CommandClassFrame Frame { get; } + + public static IReadOnlySet Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 1) + { + logger.LogWarning("Humidity Control Setpoint Supported Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Humidity Control Setpoint Supported Report frame is too short"); + } + + return BitMaskHelper.ParseBitMask(frame.CommandParameters.Span, startBit: 1); + } + } +} diff --git a/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.cs b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.cs new file mode 100644 index 0000000..d227a9e --- /dev/null +++ b/src/ZWave.CommandClasses/HumidityControlSetpointCommandClass.cs @@ -0,0 +1,194 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// The humidity control setpoint type. +/// +public enum HumidityControlSetpointType : byte +{ + /// + /// Humidifier setpoint. + /// + Humidifier = 0x01, + + /// + /// De-humidifier setpoint. + /// + Dehumidifier = 0x02, + + /// + /// Auto setpoint. + /// + Auto = 0x03, +} + +/// +/// The humidity control setpoint scale. +/// +public enum HumidityControlSetpointScale : byte +{ + /// + /// Percentage value. + /// + Percentage = 0x00, + + /// + /// Absolute humidity (g/m³). + /// + AbsoluteHumidity = 0x01, +} + +/// +/// Commands for the Humidity Control Setpoint Command Class. +/// +public enum HumidityControlSetpointCommand : byte +{ + /// + /// Set a humidity control setpoint in the device. + /// + Set = 0x01, + + /// + /// Request the given humidity control setpoint type from the device. + /// + Get = 0x02, + + /// + /// Report the value of the humidity control setpoint type from the device. + /// + Report = 0x03, + + /// + /// Request the humidity control setpoint types supported by the device. + /// + SupportedGet = 0x04, + + /// + /// Report the humidity control setpoint types supported by the device. + /// + SupportedReport = 0x05, + + /// + /// Request the supported scales for a given setpoint type. + /// + ScaleSupportedGet = 0x06, + + /// + /// Report the supported scales for a given setpoint type. + /// + ScaleSupportedReport = 0x07, + + /// + /// Request the minimum and maximum setpoint values for a given setpoint type. + /// + CapabilitiesGet = 0x08, + + /// + /// Report the minimum and maximum setpoint values for a given setpoint type. + /// + CapabilitiesReport = 0x09, +} + +/// +/// Implements the Humidity Control Setpoint Command Class (V1-2). +/// +[CommandClass(CommandClassId.HumidityControlSetpoint)] +public sealed partial class HumidityControlSetpointCommandClass : CommandClass +{ + internal HumidityControlSetpointCommandClass( + CommandClassInfo info, + IDriver driver, + IEndpoint endpoint, + ILogger logger) + : base(info, driver, endpoint, logger) + { + } + + /// + public override bool? IsCommandSupported(HumidityControlSetpointCommand command) + => command switch + { + HumidityControlSetpointCommand.Set => true, + HumidityControlSetpointCommand.Get => true, + HumidityControlSetpointCommand.SupportedGet => true, + HumidityControlSetpointCommand.ScaleSupportedGet => true, + HumidityControlSetpointCommand.CapabilitiesGet => true, + _ => false, + }; + + /// + internal override async Task InterviewAsync(CancellationToken cancellationToken) + { + IReadOnlySet supportedTypes = await GetSupportedAsync(cancellationToken).ConfigureAwait(false); + + foreach (HumidityControlSetpointType setpointType in supportedTypes) + { + _ = await GetScaleSupportedAsync(setpointType, cancellationToken).ConfigureAwait(false); + _ = await GetCapabilitiesAsync(setpointType, cancellationToken).ConfigureAwait(false); + _ = await GetAsync(setpointType, cancellationToken).ConfigureAwait(false); + } + } + + /// + protected override void ProcessUnsolicitedCommand(CommandClassFrame frame) + { + switch ((HumidityControlSetpointCommand)frame.CommandId) + { + case HumidityControlSetpointCommand.Report: + { + HumidityControlSetpointReport report = HumidityControlSetpointReportCommand.Parse(frame, Logger); + _setpointValues[report.SetpointType] = report; + OnSetpointReportReceived?.Invoke(report); + break; + } + } + } + + /// + /// Parse a precision/scale/size byte and extract the value from the following bytes. + /// + internal static (int Precision, HumidityControlSetpointScale Scale, int ValueSize) ParsePrecisionScaleSize(byte pss) + { + int precision = (pss & 0b1110_0000) >> 5; + HumidityControlSetpointScale scale = (HumidityControlSetpointScale)((pss & 0b0001_1000) >> 3); + int valueSize = pss & 0b0000_0111; + return (precision, scale, valueSize); + } + + /// + /// Parse a signed big-endian value of 1, 2, or 4 bytes with the given precision. + /// + internal static double ParseValue(ReadOnlySpan valueBytes, int precision) + { + int rawValue = valueBytes.ReadSignedVariableSizeBE(); + return rawValue / BinaryExtensions.PowersOfTen[precision]; + } + + /// + /// Encode a precision/scale/size byte. + /// + internal static byte EncodePrecisionScaleSize(int precision, HumidityControlSetpointScale scale, int valueSize) + { + return (byte)(((precision & 0x07) << 5) | (((byte)scale & 0x03) << 3) | (valueSize & 0x07)); + } + + /// + /// Determine the precision and raw integer value for a decimal setpoint value. + /// + internal static (int RawValue, int Size, int Precision) EncodeValue(double value) + { + // Determine precision: count decimal places (up to 7) + int precision = 0; + double scaled = value; + while (precision < 7 && Math.Abs(scaled - Math.Round(scaled)) > 1e-9) + { + precision++; + scaled = value * BinaryExtensions.PowersOfTen[precision]; + } + + int rawValue = (int)Math.Round(value * BinaryExtensions.PowersOfTen[precision]); + int valueSize = rawValue.GetSignedVariableSize(); + return (rawValue, valueSize, precision); + } +} diff --git a/src/ZWave.CommandClasses/MultilevelSensorCommandClass.Report.cs b/src/ZWave.CommandClasses/MultilevelSensorCommandClass.Report.cs index 8662253..8cf2d4b 100644 --- a/src/ZWave.CommandClasses/MultilevelSensorCommandClass.Report.cs +++ b/src/ZWave.CommandClasses/MultilevelSensorCommandClass.Report.cs @@ -1,4 +1,3 @@ -using System.Buffers.Binary; using Microsoft.Extensions.Logging; namespace ZWave.CommandClasses; @@ -184,18 +183,8 @@ public static MultilevelSensorReport Parse(CommandClassFrame frame, ILogger logg ReadOnlySpan valueBytes = span.Slice(2, valueSize); - // The spec (CC:0031.01.05.11.006) says Size MUST be 1, 2, or 4. - // Values are signed big-endian (two's complement). - int rawValue = valueSize switch - { - 1 => (sbyte)valueBytes[0], - 2 => BinaryPrimitives.ReadInt16BigEndian(valueBytes), - 4 => BinaryPrimitives.ReadInt32BigEndian(valueBytes), - _ => throw new ZWaveException( - ZWaveErrorCode.InvalidPayload, - $"Invalid sensor value size {valueSize}. Expected 1, 2, or 4."), - }; - double value = rawValue / Math.Pow(10, precision); + int rawValue = valueBytes.ReadSignedVariableSizeBE(); + double value = rawValue / BinaryExtensions.PowersOfTen[precision]; return new MultilevelSensorReport(sensorType, scale, value); } diff --git a/src/ZWave.CommandClasses/MultilevelSensorCommandClass.SupportedSensor.cs b/src/ZWave.CommandClasses/MultilevelSensorCommandClass.SupportedSensor.cs index 834fc58..91fd94d 100644 --- a/src/ZWave.CommandClasses/MultilevelSensorCommandClass.SupportedSensor.cs +++ b/src/ZWave.CommandClasses/MultilevelSensorCommandClass.SupportedSensor.cs @@ -90,23 +90,8 @@ public static IReadOnlySet Parse(CommandClassFrame frame, throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Multilevel Sensor Supported Sensor Report frame is too short"); } - HashSet supportedSensorTypes = new HashSet(); - - ReadOnlySpan bitMask = frame.CommandParameters.Span; - for (int byteNum = 0; byteNum < bitMask.Length; byteNum++) - { - for (int bitNum = 0; bitNum < 8; bitNum++) - { - if ((bitMask[byteNum] & (1 << bitNum)) != 0) - { - // As per the spec, bit 0 corresponds to Sensor Type 0x01, so we need to add 1. - MultilevelSensorType sensorType = (MultilevelSensorType)((byteNum << 3) + bitNum + 1); - supportedSensorTypes.Add(sensorType); - } - } - } - - return supportedSensorTypes; + // As per the spec, bit 0 corresponds to Sensor Type 0x01, so offset by 1. + return BitMaskHelper.ParseBitMask(frame.CommandParameters.Span, offset: 1); } } } diff --git a/src/ZWave.CommandClasses/NotificationCommandClass.Supported.cs b/src/ZWave.CommandClasses/NotificationCommandClass.Supported.cs index 79046eb..ecc9b8f 100644 --- a/src/ZWave.CommandClasses/NotificationCommandClass.Supported.cs +++ b/src/ZWave.CommandClasses/NotificationCommandClass.Supported.cs @@ -107,7 +107,6 @@ public static SupportedNotifications Parse(CommandClassFrame frame, ILogger logg bool supportsV1Alarm = (frame.CommandParameters.Span[0] & 0b1000_0000) != 0; - HashSet supportedNotificationTypes = []; int numBitMasks = frame.CommandParameters.Span[0] & 0b0001_1111; if (frame.CommandParameters.Length < 1 + numBitMasks) @@ -117,18 +116,7 @@ public static SupportedNotifications Parse(CommandClassFrame frame, ILogger logg throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Notification Supported Report bitmask is truncated"); } - ReadOnlySpan bitMask = frame.CommandParameters.Span.Slice(1, numBitMasks); - for (int byteNum = 0; byteNum < bitMask.Length; byteNum++) - { - for (int bitNum = 0; bitNum < 8; bitNum++) - { - if ((bitMask[byteNum] & (1 << bitNum)) != 0) - { - NotificationType notificationType = (NotificationType)((byteNum << 3) + bitNum); - supportedNotificationTypes.Add(notificationType); - } - } - } + HashSet supportedNotificationTypes = BitMaskHelper.ParseBitMask(frame.CommandParameters.Span.Slice(1, numBitMasks)); return new SupportedNotifications(supportsV1Alarm, supportedNotificationTypes); } diff --git a/src/ZWave.Serial/Commands/GetProtocolVersion.cs b/src/ZWave.Serial/Commands/GetProtocolVersion.cs index b50b124..e1fdbdf 100644 --- a/src/ZWave.Serial/Commands/GetProtocolVersion.cs +++ b/src/ZWave.Serial/Commands/GetProtocolVersion.cs @@ -88,7 +88,7 @@ public GetProtocolVersionResponse(DataFrame frame) /// The application framework build number. The value 0 indicates this value is not available. /// public ushort ApplicationFrameworkBuildNumber - => BinaryPrimitives.ReadUInt16BigEndian(Frame.CommandParameters.Span.Slice(4, 2)); + => Frame.CommandParameters.Span.Slice(4, 2).ToUInt16BE(); /// /// The git commit hash for the Z-Wave Protocol running in the Z-Wave API Module. diff --git a/src/ZWave.Serial/NodeIdType.cs b/src/ZWave.Serial/NodeIdType.cs index a59bd6a..d59ae33 100644 --- a/src/ZWave.Serial/NodeIdType.cs +++ b/src/ZWave.Serial/NodeIdType.cs @@ -46,7 +46,7 @@ public static int WriteNodeId(this NodeIdType nodeIdType, Span buffer, int { if (nodeIdType == NodeIdType.Long) { - BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(offset, 2), nodeId); + nodeId.WriteBytesBE(buffer.Slice(offset, 2)); return offset + 2; } @@ -62,7 +62,7 @@ public static ushort ReadNodeId(this NodeIdType nodeIdType, ReadOnlySpan b { if (nodeIdType == NodeIdType.Long) { - return BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(offset, 2)); + return buffer.Slice(offset, 2).ToUInt16BE(); } return buffer[offset];