diff --git a/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.EventSignal.cs b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.EventSignal.cs new file mode 100644 index 0000000..755abdf --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.EventSignal.cs @@ -0,0 +1,106 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace ZWave.CommandClasses.Tests; + +public partial class BarrierOperatorCommandClassTests +{ + [TestMethod] + public void EventSignalSetCommand_Create_On() + { + var command = BarrierOperatorCommandClass.EventSignalSetCommand.Create( + BarrierOperatorSignalingSubsystemType.AudibleNotification, + on: true); + + Assert.AreEqual(4, command.Frame.Data.Length); + Assert.AreEqual(CommandClassId.BarrierOperator, command.Frame.CommandClassId); + Assert.AreEqual((byte)BarrierOperatorCommand.EventSignalSet, command.Frame.CommandId); + Assert.AreEqual((byte)BarrierOperatorSignalingSubsystemType.AudibleNotification, command.Frame.CommandParameters.Span[0]); + Assert.AreEqual((byte)0xFF, command.Frame.CommandParameters.Span[1]); + } + + [TestMethod] + public void EventSignalSetCommand_Create_Off() + { + var command = BarrierOperatorCommandClass.EventSignalSetCommand.Create( + BarrierOperatorSignalingSubsystemType.VisualNotification, + on: false); + + Assert.AreEqual(4, command.Frame.Data.Length); + Assert.AreEqual((byte)BarrierOperatorSignalingSubsystemType.VisualNotification, command.Frame.CommandParameters.Span[0]); + Assert.AreEqual((byte)0x00, command.Frame.CommandParameters.Span[1]); + } + + [TestMethod] + public void EventSignalingGetCommand_Create() + { + var command = BarrierOperatorCommandClass.EventSignalingGetCommand.Create( + BarrierOperatorSignalingSubsystemType.AudibleNotification); + + Assert.AreEqual(3, command.Frame.Data.Length); + Assert.AreEqual(CommandClassId.BarrierOperator, command.Frame.CommandClassId); + Assert.AreEqual((byte)BarrierOperatorCommand.EventSignalingGet, command.Frame.CommandId); + Assert.AreEqual((byte)BarrierOperatorSignalingSubsystemType.AudibleNotification, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void EventSignalingReportCommand_Parse_On() + { + byte[] data = [0x66, 0x08, 0x01, 0xFF]; + CommandClassFrame frame = new(data); + + BarrierOperatorEventSignalReport report = + BarrierOperatorCommandClass.EventSignalingReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorSignalingSubsystemType.AudibleNotification, report.SubsystemType); + Assert.AreEqual((byte)0xFF, report.SubsystemState); + Assert.IsTrue(report.IsOn); + } + + [TestMethod] + public void EventSignalingReportCommand_Parse_Off() + { + byte[] data = [0x66, 0x08, 0x02, 0x00]; + CommandClassFrame frame = new(data); + + BarrierOperatorEventSignalReport report = + BarrierOperatorCommandClass.EventSignalingReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorSignalingSubsystemType.VisualNotification, report.SubsystemType); + Assert.AreEqual((byte)0x00, report.SubsystemState); + Assert.IsFalse(report.IsOn); + } + + [TestMethod] + public void EventSignalingReportCommand_Parse_ReservedState() + { + byte[] data = [0x66, 0x08, 0x01, 0x55]; + CommandClassFrame frame = new(data); + + BarrierOperatorEventSignalReport report = + BarrierOperatorCommandClass.EventSignalingReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorSignalingSubsystemType.AudibleNotification, report.SubsystemType); + Assert.AreEqual((byte)0x55, report.SubsystemState); + Assert.IsNull(report.IsOn); + } + + [TestMethod] + public void EventSignalingReportCommand_Parse_TooShort_Throws() + { + byte[] data = [0x66, 0x08, 0x01]; + CommandClassFrame frame = new(data); + + Assert.ThrowsExactly( + () => BarrierOperatorCommandClass.EventSignalingReportCommand.Parse(frame, NullLogger.Instance)); + } + + [TestMethod] + public void EventSignalingReportCommand_Parse_Empty_Throws() + { + byte[] data = [0x66, 0x08]; + CommandClassFrame frame = new(data); + + Assert.ThrowsExactly( + () => BarrierOperatorCommandClass.EventSignalingReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.Report.cs b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.Report.cs new file mode 100644 index 0000000..f0f3dda --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.Report.cs @@ -0,0 +1,157 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace ZWave.CommandClasses.Tests; + +public partial class BarrierOperatorCommandClassTests +{ + [TestMethod] + public void SetCommand_Create_Open() + { + var command = BarrierOperatorCommandClass.BarrierOperatorSetCommand.Create(BarrierOperatorTargetValue.Open); + + Assert.AreEqual(3, command.Frame.Data.Length); + Assert.AreEqual(CommandClassId.BarrierOperator, command.Frame.CommandClassId); + Assert.AreEqual((byte)BarrierOperatorCommand.Set, command.Frame.CommandId); + Assert.AreEqual((byte)0xFF, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void SetCommand_Create_Close() + { + var command = BarrierOperatorCommandClass.BarrierOperatorSetCommand.Create(BarrierOperatorTargetValue.Close); + + Assert.AreEqual(3, command.Frame.Data.Length); + Assert.AreEqual((byte)0x00, command.Frame.CommandParameters.Span[0]); + } + + [TestMethod] + public void GetCommand_Create() + { + var command = BarrierOperatorCommandClass.BarrierOperatorGetCommand.Create(); + + Assert.AreEqual(2, command.Frame.Data.Length); + Assert.AreEqual(CommandClassId.BarrierOperator, command.Frame.CommandClassId); + Assert.AreEqual((byte)BarrierOperatorCommand.Get, command.Frame.CommandId); + } + + [TestMethod] + public void ReportCommand_Parse_Closed() + { + byte[] data = [0x66, 0x03, 0x00]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual((byte)0x00, report.StateValue); + Assert.AreEqual(BarrierOperatorState.Closed, report.State); + Assert.IsNull(report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_Open() + { + byte[] data = [0x66, 0x03, 0xFF]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual((byte)0xFF, report.StateValue); + Assert.AreEqual(BarrierOperatorState.Open, report.State); + Assert.IsNull(report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_Opening() + { + byte[] data = [0x66, 0x03, 0xFE]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorState.Opening, report.State); + Assert.IsNull(report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_Closing() + { + byte[] data = [0x66, 0x03, 0xFC]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorState.Closing, report.State); + Assert.IsNull(report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_Stopped() + { + byte[] data = [0x66, 0x03, 0xFD]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorState.Stopped, report.State); + Assert.IsNull(report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_StoppedAtPosition_50Percent() + { + byte[] data = [0x66, 0x03, 0x32]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorState.Stopped, report.State); + Assert.AreEqual((byte)50, report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_StoppedAtPosition_1Percent() + { + byte[] data = [0x66, 0x03, 0x01]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorState.Stopped, report.State); + Assert.AreEqual((byte)1, report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_StoppedAtPosition_99Percent() + { + byte[] data = [0x66, 0x03, 0x63]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual(BarrierOperatorState.Stopped, report.State); + Assert.AreEqual((byte)99, report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_ReservedValue() + { + byte[] data = [0x66, 0x03, 0xAA]; + CommandClassFrame frame = new(data); + + BarrierOperatorReport report = BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance); + + Assert.AreEqual((byte)0xAA, report.StateValue); + Assert.IsNull(report.State); + Assert.IsNull(report.Position); + } + + [TestMethod] + public void ReportCommand_Parse_TooShort_Throws() + { + byte[] data = [0x66, 0x03]; + CommandClassFrame frame = new(data); + + Assert.ThrowsExactly( + () => BarrierOperatorCommandClass.BarrierOperatorReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.SignalingCapabilities.cs b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.SignalingCapabilities.cs new file mode 100644 index 0000000..245fe7a --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.SignalingCapabilities.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace ZWave.CommandClasses.Tests; + +public partial class BarrierOperatorCommandClassTests +{ + [TestMethod] + public void SignalSupportedGetCommand_Create() + { + var command = BarrierOperatorCommandClass.SignalSupportedGetCommand.Create(); + + Assert.AreEqual(2, command.Frame.Data.Length); + Assert.AreEqual(CommandClassId.BarrierOperator, command.Frame.CommandClassId); + Assert.AreEqual((byte)BarrierOperatorCommand.SignalSupportedGet, command.Frame.CommandId); + } + + [TestMethod] + public void SignalSupportedReportCommand_Parse_BothSubsystems() + { + // Bit 0 = type 0x01 (Audible), bit 1 = type 0x02 (Visual) → 0x03 + byte[] data = [0x66, 0x05, 0x03]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = + BarrierOperatorCommandClass.SignalSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(2, supported); + Assert.Contains(BarrierOperatorSignalingSubsystemType.AudibleNotification, supported); + Assert.Contains(BarrierOperatorSignalingSubsystemType.VisualNotification, supported); + } + + [TestMethod] + public void SignalSupportedReportCommand_Parse_AudibleOnly() + { + // Bit 0 = type 0x01 (Audible) → 0x01 + byte[] data = [0x66, 0x05, 0x01]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = + BarrierOperatorCommandClass.SignalSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(1, supported); + Assert.Contains(BarrierOperatorSignalingSubsystemType.AudibleNotification, supported); + } + + [TestMethod] + public void SignalSupportedReportCommand_Parse_VisualOnly() + { + // Bit 1 = type 0x02 (Visual) → 0x02 + byte[] data = [0x66, 0x05, 0x02]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = + BarrierOperatorCommandClass.SignalSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(1, supported); + Assert.Contains(BarrierOperatorSignalingSubsystemType.VisualNotification, supported); + } + + [TestMethod] + public void SignalSupportedReportCommand_Parse_NoneSupported() + { + // All bits zero — no subsystems supported + byte[] data = [0x66, 0x05, 0x00]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = + BarrierOperatorCommandClass.SignalSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.IsEmpty(supported); + } + + [TestMethod] + public void SignalSupportedReportCommand_Parse_MultiByteMask() + { + // 2-byte bitmask: byte 0 = 0x03 (types 1,2), byte 1 = 0x01 (type 9) + byte[] data = [0x66, 0x05, 0x03, 0x01]; + CommandClassFrame frame = new(data); + + IReadOnlySet supported = + BarrierOperatorCommandClass.SignalSupportedReportCommand.Parse(frame, NullLogger.Instance); + + Assert.HasCount(3, supported); + Assert.Contains(BarrierOperatorSignalingSubsystemType.AudibleNotification, supported); + Assert.Contains(BarrierOperatorSignalingSubsystemType.VisualNotification, supported); + Assert.Contains((BarrierOperatorSignalingSubsystemType)9, supported); + } + + [TestMethod] + public void SignalSupportedReportCommand_Parse_TooShort_Throws() + { + byte[] data = [0x66, 0x05]; + CommandClassFrame frame = new(data); + + Assert.ThrowsExactly( + () => BarrierOperatorCommandClass.SignalSupportedReportCommand.Parse(frame, NullLogger.Instance)); + } +} diff --git a/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.cs b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.cs new file mode 100644 index 0000000..b1432b3 --- /dev/null +++ b/src/ZWave.CommandClasses.Tests/BarrierOperatorCommandClassTests.cs @@ -0,0 +1,6 @@ +namespace ZWave.CommandClasses.Tests; + +[TestClass] +public partial class BarrierOperatorCommandClassTests +{ +} diff --git a/src/ZWave.CommandClasses/BarrierOperatorCommandClass.EventSignal.cs b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.EventSignal.cs new file mode 100644 index 0000000..330fbcd --- /dev/null +++ b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.EventSignal.cs @@ -0,0 +1,151 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// Represents the state of a signaling subsystem on a barrier operator device. +/// +/// The type of signaling subsystem. +/// The raw state byte value (0x00 = Off, 0xFF = On). +public readonly record struct BarrierOperatorEventSignalReport( + BarrierOperatorSignalingSubsystemType SubsystemType, + byte SubsystemState) +{ + /// + /// Gets whether the subsystem is on, off, or for reserved values. + /// + public bool? IsOn => SubsystemState switch + { + 0x00 => false, + 0xFF => true, + _ => null, + }; +} + +public sealed partial class BarrierOperatorCommandClass +{ + private readonly Dictionary _eventSignals = []; + + /// + /// Occurs when a Barrier Operator Event Signaling Report is received, whether solicited or unsolicited. + /// + public event Action? OnEventSignalReportReceived; + + /// + /// Gets the last known state for each signaling subsystem type that has been reported. + /// + public IReadOnlyDictionary EventSignals => _eventSignals; + + /// + /// Gets the state of a signaling subsystem on the device. + /// + /// The type of signaling subsystem to query. + /// A cancellation token. + public async Task GetEventSignalAsync( + BarrierOperatorSignalingSubsystemType subsystemType, + CancellationToken cancellationToken) + { + EventSignalingGetCommand command = EventSignalingGetCommand.Create(subsystemType); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync( + predicate: frame => frame.CommandParameters.Length > 0 + && (BarrierOperatorSignalingSubsystemType)frame.CommandParameters.Span[0] == subsystemType, + cancellationToken).ConfigureAwait(false); + BarrierOperatorEventSignalReport report = EventSignalingReportCommand.Parse(reportFrame, Logger); + _eventSignals[report.SubsystemType] = report; + OnEventSignalReportReceived?.Invoke(report); + return report; + } + + /// + /// Turns on or off an event signaling subsystem on the device. + /// + /// The type of signaling subsystem to control. + /// to turn on the subsystem; to turn it off. + /// A cancellation token. + public async Task SetEventSignalAsync( + BarrierOperatorSignalingSubsystemType subsystemType, + bool on, + CancellationToken cancellationToken) + { + EventSignalSetCommand command = EventSignalSetCommand.Create(subsystemType, on); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + } + + internal readonly struct EventSignalSetCommand : ICommand + { + public EventSignalSetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.EventSignalSet; + + public CommandClassFrame Frame { get; } + + public static EventSignalSetCommand Create( + BarrierOperatorSignalingSubsystemType subsystemType, + bool on) + { + ReadOnlySpan commandParameters = [(byte)subsystemType, on ? (byte)0xFF : (byte)0x00]; + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new EventSignalSetCommand(frame); + } + } + + internal readonly struct EventSignalingGetCommand : ICommand + { + public EventSignalingGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.EventSignalingGet; + + public CommandClassFrame Frame { get; } + + public static EventSignalingGetCommand Create(BarrierOperatorSignalingSubsystemType subsystemType) + { + ReadOnlySpan commandParameters = [(byte)subsystemType]; + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new EventSignalingGetCommand(frame); + } + } + + internal readonly struct EventSignalingReportCommand : ICommand + { + public EventSignalingReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.EventSignalingReport; + + public CommandClassFrame Frame { get; } + + public static BarrierOperatorEventSignalReport Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 2) + { + logger.LogWarning( + "Barrier Operator Event Signaling Report frame is too short ({Length} bytes)", + frame.CommandParameters.Length); + throw new ZWaveException( + ZWaveErrorCode.InvalidPayload, + "Barrier Operator Event Signaling Report frame is too short"); + } + + ReadOnlySpan span = frame.CommandParameters.Span; + BarrierOperatorSignalingSubsystemType subsystemType = (BarrierOperatorSignalingSubsystemType)span[0]; + byte subsystemState = span[1]; + + return new BarrierOperatorEventSignalReport(subsystemType, subsystemState); + } + } +} diff --git a/src/ZWave.CommandClasses/BarrierOperatorCommandClass.Report.cs b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.Report.cs new file mode 100644 index 0000000..6501998 --- /dev/null +++ b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.Report.cs @@ -0,0 +1,151 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// Represents the target value for a Barrier Operator Set command. +/// +public enum BarrierOperatorTargetValue : byte +{ + /// + /// Initiate unattended close. + /// + Close = 0x00, + + /// + /// Initiate unattended open. + /// + Open = 0xFF, +} + +/// +/// Represents a Barrier Operator Report received from a device. +/// +/// The raw state byte value from the device. +public readonly record struct BarrierOperatorReport(byte StateValue) +{ + /// + /// Gets the interpreted barrier state, or for reserved values. + /// + public BarrierOperatorState? State => StateValue switch + { + 0x00 => BarrierOperatorState.Closed, + >= 0x01 and <= 0x63 => BarrierOperatorState.Stopped, + 0xFC => BarrierOperatorState.Closing, + 0xFD => BarrierOperatorState.Stopped, + 0xFE => BarrierOperatorState.Opening, + 0xFF => BarrierOperatorState.Open, + _ => null, + }; + + /// + /// Gets the exact position percentage (1-99) when the barrier is stopped at a known position, + /// or otherwise. + /// + public byte? Position => StateValue is >= 0x01 and <= 0x63 ? StateValue : null; +} + +public sealed partial class BarrierOperatorCommandClass +{ + /// + /// Occurs when a Barrier Operator Report is received, whether solicited or unsolicited. + /// + public event Action? OnBarrierOperatorReportReceived; + + /// + /// Gets the last Barrier Operator Report received from the device. + /// + public BarrierOperatorReport? LastReport { get; private set; } + + /// + /// Gets the current state of the barrier operator device. + /// + public async Task GetAsync(CancellationToken cancellationToken) + { + BarrierOperatorGetCommand command = BarrierOperatorGetCommand.Create(); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync(cancellationToken).ConfigureAwait(false); + BarrierOperatorReport report = BarrierOperatorReportCommand.Parse(reportFrame, Logger); + LastReport = report; + OnBarrierOperatorReportReceived?.Invoke(report); + return report; + } + + /// + /// Initiates an unattended change in state of the barrier. + /// + /// The intended state of the barrier. + /// A cancellation token. + public async Task SetAsync(BarrierOperatorTargetValue targetValue, CancellationToken cancellationToken) + { + BarrierOperatorSetCommand command = BarrierOperatorSetCommand.Create(targetValue); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + } + + internal readonly struct BarrierOperatorSetCommand : ICommand + { + public BarrierOperatorSetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.Set; + + public CommandClassFrame Frame { get; } + + public static BarrierOperatorSetCommand Create(BarrierOperatorTargetValue targetValue) + { + ReadOnlySpan commandParameters = [(byte)targetValue]; + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId, commandParameters); + return new BarrierOperatorSetCommand(frame); + } + } + + internal readonly struct BarrierOperatorGetCommand : ICommand + { + public BarrierOperatorGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.Get; + + public CommandClassFrame Frame { get; } + + public static BarrierOperatorGetCommand Create() + { + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId); + return new BarrierOperatorGetCommand(frame); + } + } + + internal readonly struct BarrierOperatorReportCommand : ICommand + { + public BarrierOperatorReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.Report; + + public CommandClassFrame Frame { get; } + + public static BarrierOperatorReport Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 1) + { + logger.LogWarning("Barrier Operator Report frame is too short ({Length} bytes)", frame.CommandParameters.Length); + throw new ZWaveException(ZWaveErrorCode.InvalidPayload, "Barrier Operator Report frame is too short"); + } + + byte stateValue = frame.CommandParameters.Span[0]; + return new BarrierOperatorReport(stateValue); + } + } +} diff --git a/src/ZWave.CommandClasses/BarrierOperatorCommandClass.SignalingCapabilities.cs b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.SignalingCapabilities.cs new file mode 100644 index 0000000..60ed584 --- /dev/null +++ b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.SignalingCapabilities.cs @@ -0,0 +1,96 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +public sealed partial class BarrierOperatorCommandClass +{ + /// + /// Gets the signaling subsystem types supported by this device, + /// or if not yet queried. + /// + public IReadOnlySet? SupportedSignalingSubsystems { get; private set; } + + /// + /// Queries the device for its supported signaling subsystem types. + /// + public async Task> GetSupportedSignalingSubsystemsAsync( + CancellationToken cancellationToken) + { + SignalSupportedGetCommand command = SignalSupportedGetCommand.Create(); + await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); + CommandClassFrame reportFrame = await AwaitNextReportAsync(cancellationToken).ConfigureAwait(false); + IReadOnlySet supported = SignalSupportedReportCommand.Parse(reportFrame, Logger); + SupportedSignalingSubsystems = supported; + return supported; + } + + internal readonly struct SignalSupportedGetCommand : ICommand + { + public SignalSupportedGetCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.SignalSupportedGet; + + public CommandClassFrame Frame { get; } + + public static SignalSupportedGetCommand Create() + { + CommandClassFrame frame = CommandClassFrame.Create(CommandClassId, CommandId); + return new SignalSupportedGetCommand(frame); + } + } + + internal readonly struct SignalSupportedReportCommand : ICommand + { + public SignalSupportedReportCommand(CommandClassFrame frame) + { + Frame = frame; + } + + public static CommandClassId CommandClassId => CommandClassId.BarrierOperator; + + public static byte CommandId => (byte)BarrierOperatorCommand.SignalSupportedReport; + + public CommandClassFrame Frame { get; } + + /// + /// Parses the signaling capabilities bitmask. + /// Per spec, bit 0 of byte 0 indicates subsystem type 0x01, bit 1 indicates type 0x02, etc. + /// + public static IReadOnlySet Parse(CommandClassFrame frame, ILogger logger) + { + if (frame.CommandParameters.Length < 1) + { + logger.LogWarning( + "Barrier Operator Signal Supported Report frame is too short ({Length} bytes)", + frame.CommandParameters.Length); + throw new ZWaveException( + ZWaveErrorCode.InvalidPayload, + "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; + } + } +} diff --git a/src/ZWave.CommandClasses/BarrierOperatorCommandClass.cs b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.cs new file mode 100644 index 0000000..4ea7aa8 --- /dev/null +++ b/src/ZWave.CommandClasses/BarrierOperatorCommandClass.cs @@ -0,0 +1,159 @@ +using Microsoft.Extensions.Logging; + +namespace ZWave.CommandClasses; + +/// +/// Commands for the Barrier Operator Command Class. +/// +public enum BarrierOperatorCommand : byte +{ + /// + /// Initiate an unattended change in state of the barrier. + /// + Set = 0x01, + + /// + /// Request the current state of a barrier operator device. + /// + Get = 0x02, + + /// + /// Advertise the status of the barrier operator device. + /// + Report = 0x03, + + /// + /// Query a device for available signaling subsystems which may be controlled via Z-Wave. + /// + SignalSupportedGet = 0x04, + + /// + /// Report the signaling subsystems supported by the device. + /// + SignalSupportedReport = 0x05, + + /// + /// Turn on or off an event signaling subsystem that is supported by the device. + /// + EventSignalSet = 0x06, + + /// + /// Request the state of a signaling subsystem. + /// + EventSignalingGet = 0x07, + + /// + /// Indicate the state of a notification subsystem of a Barrier Device. + /// + EventSignalingReport = 0x08, +} + +/// +/// Represents the interpreted state of a barrier operator device. +/// +public enum BarrierOperatorState : byte +{ + /// + /// The barrier is in the Closed position. + /// + Closed = 0x00, + + /// + /// The barrier is closing. The current position is unknown. + /// + Closing = 0xFC, + + /// + /// The barrier is stopped. The current position may or may not be known. + /// + Stopped = 0xFD, + + /// + /// The barrier is opening. The current position is unknown. + /// + Opening = 0xFE, + + /// + /// The barrier is in the Open position. + /// + Open = 0xFF, +} + +/// +/// Represents the type of a signaling subsystem on a barrier operator device. +/// +public enum BarrierOperatorSignalingSubsystemType : byte +{ + /// + /// The Barrier Device has an Audible Notification subsystem (e.g. Siren). + /// + AudibleNotification = 0x01, + + /// + /// The Barrier Device has a Visual Notification subsystem (e.g. Flashing Light). + /// + VisualNotification = 0x02, +} + +/// +/// The Barrier Operator Command Class is used to control and query the status of motorized barriers. +/// +[CommandClass(CommandClassId.BarrierOperator)] +public sealed partial class BarrierOperatorCommandClass : CommandClass +{ + internal BarrierOperatorCommandClass( + CommandClassInfo info, + IDriver driver, + IEndpoint endpoint, + ILogger logger) + : base(info, driver, endpoint, logger) + { + } + + /// + public override bool? IsCommandSupported(BarrierOperatorCommand command) + => command switch + { + BarrierOperatorCommand.Set => true, + BarrierOperatorCommand.Get => true, + BarrierOperatorCommand.SignalSupportedGet => true, + BarrierOperatorCommand.EventSignalSet => true, + BarrierOperatorCommand.EventSignalingGet => true, + _ => false, + }; + + /// + internal override async Task InterviewAsync(CancellationToken cancellationToken) + { + _ = await GetAsync(cancellationToken).ConfigureAwait(false); + IReadOnlySet supportedSubsystems = + await GetSupportedSignalingSubsystemsAsync(cancellationToken).ConfigureAwait(false); + + foreach (BarrierOperatorSignalingSubsystemType subsystemType in supportedSubsystems) + { + _ = await GetEventSignalAsync(subsystemType, cancellationToken).ConfigureAwait(false); + } + } + + /// + protected override void ProcessUnsolicitedCommand(CommandClassFrame frame) + { + switch ((BarrierOperatorCommand)frame.CommandId) + { + case BarrierOperatorCommand.Report: + { + BarrierOperatorReport report = BarrierOperatorReportCommand.Parse(frame, Logger); + LastReport = report; + OnBarrierOperatorReportReceived?.Invoke(report); + break; + } + case BarrierOperatorCommand.EventSignalingReport: + { + BarrierOperatorEventSignalReport report = EventSignalingReportCommand.Parse(frame, Logger); + _eventSignals[report.SubsystemType] = report; + OnEventSignalReportReceived?.Invoke(report); + break; + } + } + } +}