From babfebf83bb86b040a843d1a64ded718d8652d45 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:05:39 +0200 Subject: [PATCH 01/15] chore(deps): update NuGet package versions in `Directory.Packages.props` - Bumped `CreativeCoders.*` packages to version `6.7.2`. - Updated `Spectre.Console` to version `0.55.2`. - Reformatted the file for consistency. --- Directory.Packages.props | 57 +++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 07b02be..b1c1280 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,31 +1,28 @@ - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 1bac142dd4b3ba96c5644eccad1934a5e397a7c9 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:54:11 +0200 Subject: [PATCH 02/15] refactor(core): reorganize `CreativeCoders.HomeMatic` namespaces and extract XML-RPC-specific functionality - Moved XML-RPC types and related imports from `CreativeCoders.HomeMatic.Core` to a dedicated `CreativeCoders.HomeMatic.XmlRpc` namespace. - Adjusted project references and imports for consistency across files. - Introduced new utility `CcuDeviceKindExtensions` to map device kinds to corresponding RPC ports. - Updated unit tests to reflect namespace changes and ensure compatibility. --- .../CcuDeviceKindExtensions.cs | 21 +++++++++++++++++++ .../CreativeCoders.HomeMatic.Core.csproj | 4 ++++ .../Devices/CcuParameterDescription.cs | 2 +- .../Devices/ICcuDeviceChannelData.cs | 2 ++ .../Devices/ICcuDeviceData.cs | 4 +++- .../CcuRpcPorts.cs | 16 ++------------ .../Client/HomeMaticXmlRpcExceptionHandler.cs | 2 +- ...DeviceFirmwareUpdateStateValueConverter.cs | 2 +- .../ParameterDataTypeValueConverter.cs | 2 +- .../CreativeCoders.HomeMatic.XmlRpc.csproj | 1 - .../DeviceDescription.cs | 8 +++---- .../Devices/ChannelDirection.cs | 2 +- .../Devices/DeviceFirmwareUpdateState.cs | 2 +- .../Exceptions/CcuXmlRpcException.cs | 2 +- .../DeviceAddressExpectedException.cs | 2 +- .../Exceptions/DeviceOutOfReachException.cs | 2 +- .../Exceptions/GeneralXmlRpcException.cs | 2 +- .../Exceptions/HomeMaticException.cs | 2 +- .../InterfaceUpdateFailedException.cs | 2 +- .../Exceptions/NotEnoughDutyCycleException.cs | 2 +- .../OperationNotSupportedException.cs | 2 +- .../TransmissionOutstandingException.cs | 2 +- .../UnknownDeviceOrChannelException.cs | 2 +- .../Exceptions/UnknownParamSetException.cs | 2 +- .../UnknownParameterOrValueException.cs | 2 +- .../HomeMaticDeviceSystems.cs | 2 +- .../ParameterDescription.cs | 2 +- .../Parameters/ParameterDataType.cs | 2 +- .../Parameters/RxMode.cs | 2 +- .../XmlRpcApiAddress.cs | 2 +- source/CreativeCoders.HomeMatic/CcuDevice.cs | 3 ++- .../CcuDeviceBuilder.cs | 3 ++- .../CcuDeviceChannel.cs | 1 + .../Commanding/CliBaseCommand.cs | 1 + .../CcuClientFactoryTests.cs | 1 + .../CcuDeviceBuilderTests.cs | 3 ++- .../Exporting/DeviceExporterTests.cs | 4 +++- 37 files changed, 71 insertions(+), 47 deletions(-) create mode 100644 source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/CcuRpcPorts.cs (60%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Devices/ChannelDirection.cs (71%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Devices/DeviceFirmwareUpdateState.cs (80%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/CcuXmlRpcException.cs (81%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/DeviceAddressExpectedException.cs (79%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/DeviceOutOfReachException.cs (79%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/GeneralXmlRpcException.cs (77%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/HomeMaticException.cs (78%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/InterfaceUpdateFailedException.cs (79%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/NotEnoughDutyCycleException.cs (79%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/OperationNotSupportedException.cs (79%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/TransmissionOutstandingException.cs (80%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/UnknownDeviceOrChannelException.cs (80%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/UnknownParamSetException.cs (78%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Exceptions/UnknownParameterOrValueException.cs (80%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/HomeMaticDeviceSystems.cs (77%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Parameters/ParameterDataType.cs (74%) rename source/{CreativeCoders.HomeMatic.Core => CreativeCoders.HomeMatic.XmlRpc}/Parameters/RxMode.cs (77%) diff --git a/source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs b/source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs new file mode 100644 index 0000000..0482f0a --- /dev/null +++ b/source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs @@ -0,0 +1,21 @@ +using System; +using CreativeCoders.HomeMatic.XmlRpc; +using JetBrains.Annotations; + +namespace CreativeCoders.HomeMatic.Core; + +[PublicAPI] +public static class CcuDeviceKindExtensions +{ + public static int ToPort(this CcuDeviceKind deviceKind) + { + return deviceKind switch + { + CcuDeviceKind.HomeMatic => CcuRpcPorts.HomeMatic, + CcuDeviceKind.HomeMaticIp => CcuRpcPorts.HomeMaticIp, + CcuDeviceKind.HomeMaticWired => CcuRpcPorts.HomeMaticWired, + CcuDeviceKind.Coupled => CcuRpcPorts.CoupledDevices, + _ => throw new ArgumentOutOfRangeException(nameof(deviceKind), deviceKind, null) + }; + } +} diff --git a/source/CreativeCoders.HomeMatic.Core/CreativeCoders.HomeMatic.Core.csproj b/source/CreativeCoders.HomeMatic.Core/CreativeCoders.HomeMatic.Core.csproj index dd2cca8..47ba59f 100644 --- a/source/CreativeCoders.HomeMatic.Core/CreativeCoders.HomeMatic.Core.csproj +++ b/source/CreativeCoders.HomeMatic.Core/CreativeCoders.HomeMatic.Core.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs b/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs index 50b732c..2e19479 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using CreativeCoders.HomeMatic.Core.Parameters; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; namespace CreativeCoders.HomeMatic.Core.Devices; diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs index d6788cf..114d045 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs @@ -1,3 +1,5 @@ +using CreativeCoders.HomeMatic.XmlRpc.Devices; + namespace CreativeCoders.HomeMatic.Core.Devices; public interface ICcuDeviceChannelData : ICcuDeviceBaseData diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs index 50ac0fd..01d09bd 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs @@ -1,4 +1,6 @@ -using CreativeCoders.HomeMatic.Core.Parameters; +using CreativeCoders.HomeMatic.XmlRpc; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; namespace CreativeCoders.HomeMatic.Core.Devices; diff --git a/source/CreativeCoders.HomeMatic.Core/CcuRpcPorts.cs b/source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs similarity index 60% rename from source/CreativeCoders.HomeMatic.Core/CcuRpcPorts.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs index 3266126..e5a13f7 100644 --- a/source/CreativeCoders.HomeMatic.Core/CcuRpcPorts.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs @@ -1,7 +1,7 @@ -using System; +using System; using JetBrains.Annotations; -namespace CreativeCoders.HomeMatic.Core; +namespace CreativeCoders.HomeMatic.XmlRpc; [PublicAPI] public static class CcuRpcPorts @@ -25,16 +25,4 @@ public static int ToPort(this HomeMaticDeviceSystems deviceSystems) _ => throw new ArgumentOutOfRangeException(nameof(deviceSystems), deviceSystems, null) }; } - - public static int ToPort(this CcuDeviceKind deviceKind) - { - return deviceKind switch - { - CcuDeviceKind.HomeMatic => HomeMatic, - CcuDeviceKind.HomeMaticIp => HomeMaticIp, - CcuDeviceKind.HomeMaticWired => HomeMaticWired, - CcuDeviceKind.Coupled => CoupledDevices, - _ => throw new ArgumentOutOfRangeException(nameof(deviceKind), deviceKind, null) - }; - } } diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Client/HomeMaticXmlRpcExceptionHandler.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Client/HomeMaticXmlRpcExceptionHandler.cs index 4f2f785..85684e0 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Client/HomeMaticXmlRpcExceptionHandler.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Client/HomeMaticXmlRpcExceptionHandler.cs @@ -1,5 +1,5 @@ using System; -using CreativeCoders.HomeMatic.Core.Exceptions; +using CreativeCoders.HomeMatic.XmlRpc.Exceptions; using CreativeCoders.Net.XmlRpc.Definition; using CreativeCoders.Net.XmlRpc.Exceptions; diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/DeviceFirmwareUpdateStateValueConverter.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/DeviceFirmwareUpdateStateValueConverter.cs index c30ef04..8c25b58 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/DeviceFirmwareUpdateStateValueConverter.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/DeviceFirmwareUpdateStateValueConverter.cs @@ -1,4 +1,4 @@ -using CreativeCoders.HomeMatic.Core.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Devices; using CreativeCoders.Net.XmlRpc.Definition; using CreativeCoders.Net.XmlRpc.Model; using CreativeCoders.Net.XmlRpc.Model.Values; diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs index e29015c..9ccfdbd 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using CreativeCoders.HomeMatic.Core.Parameters; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; using CreativeCoders.Net.XmlRpc.Definition; using CreativeCoders.Net.XmlRpc.Model; using CreativeCoders.Net.XmlRpc.Model.Values; diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/CreativeCoders.HomeMatic.XmlRpc.csproj b/source/CreativeCoders.HomeMatic.XmlRpc/CreativeCoders.HomeMatic.XmlRpc.csproj index 23a50a7..91c2a8b 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/CreativeCoders.HomeMatic.XmlRpc.csproj +++ b/source/CreativeCoders.HomeMatic.XmlRpc/CreativeCoders.HomeMatic.XmlRpc.csproj @@ -13,7 +13,6 @@ - diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs b/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs index f5b4d3c..c6429e4 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using CreativeCoders.HomeMatic.Core.Devices; -using CreativeCoders.HomeMatic.Core.Parameters; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; using CreativeCoders.HomeMatic.XmlRpc.Converters; using CreativeCoders.Net.XmlRpc.Definition; using CreativeCoders.Net.XmlRpc.Definition.MemberConverters; @@ -104,7 +104,7 @@ public class DeviceDescription /// /// Gets or sets the reception mode flags for this device. Only present for BidCoS-RF devices. /// - /// A bitwise combination of values. + /// A bitwise combination of values. [XmlRpcStructMember("RX_MODE", DefaultValue = RxMode.None, Converter = typeof(FlagsMemberValueConverter))] public RxMode RxMode { get; set; } @@ -165,7 +165,7 @@ public class DeviceDescription /// /// Gets or sets the direction of this channel in a direct device link. Only present for channels. /// - /// One of the values. + /// One of the values. [XmlRpcStructMember("DIRECTION", DefaultValue = ChannelDirection.None, Converter = typeof(EnumMemberValueConverter))] public ChannelDirection ChannelDirection { get; set; } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ChannelDirection.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs similarity index 71% rename from source/CreativeCoders.HomeMatic.Core/Devices/ChannelDirection.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs index 29fa9dc..1ac2b3b 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ChannelDirection.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace CreativeCoders.HomeMatic.Core.Devices; +namespace CreativeCoders.HomeMatic.XmlRpc.Devices; [PublicAPI] public enum ChannelDirection diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/DeviceFirmwareUpdateState.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs similarity index 80% rename from source/CreativeCoders.HomeMatic.Core/Devices/DeviceFirmwareUpdateState.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs index 4e8f63c..86339c9 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/DeviceFirmwareUpdateState.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace CreativeCoders.HomeMatic.Core.Devices; +namespace CreativeCoders.HomeMatic.XmlRpc.Devices; [PublicAPI] public enum DeviceFirmwareUpdateState diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/CcuXmlRpcException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs similarity index 81% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/CcuXmlRpcException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs index 53020f2..fa2800f 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/CcuXmlRpcException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs @@ -1,7 +1,7 @@ using System; using JetBrains.Annotations; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; [PublicAPI] public abstract class CcuXmlRpcException : HomeMaticException diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceAddressExpectedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs similarity index 79% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceAddressExpectedException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs index 055b859..20a44e9 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceAddressExpectedException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class DeviceAddressExpectedException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceOutOfReachException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs similarity index 79% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceOutOfReachException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs index 20ebf16..852c869 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceOutOfReachException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class DeviceOutOfReachException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/GeneralXmlRpcException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs similarity index 77% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/GeneralXmlRpcException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs index 82fe598..cd3b2ac 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/GeneralXmlRpcException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class GeneralException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/HomeMaticException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs similarity index 78% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/HomeMaticException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs index dbeb19a..14ffd4c 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/HomeMaticException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public abstract class HomeMaticException : Exception { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/InterfaceUpdateFailedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs similarity index 79% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/InterfaceUpdateFailedException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs index 565677d..4d1d0ed 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/InterfaceUpdateFailedException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class InterfaceUpdateFailedException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/NotEnoughDutyCycleException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs similarity index 79% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/NotEnoughDutyCycleException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs index 42d7ca5..e137fb7 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/NotEnoughDutyCycleException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class NotEnoughDutyCycleException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/OperationNotSupportedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs similarity index 79% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/OperationNotSupportedException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs index e56566c..e6c7a31 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/OperationNotSupportedException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class OperationNotSupportedException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/TransmissionOutstandingException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs similarity index 80% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/TransmissionOutstandingException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs index d9a2619..8d9c551 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/TransmissionOutstandingException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class TransmissionOutstandingException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownDeviceOrChannelException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs similarity index 80% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownDeviceOrChannelException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs index 2a92922..72f2f21 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownDeviceOrChannelException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class UnknownDeviceOrChannelException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParamSetException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs similarity index 78% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParamSetException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs index dc57730..73edf16 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParamSetException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class UnknownParamSetException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParameterOrValueException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs similarity index 80% rename from source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParameterOrValueException.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs index 200beb8..f47ed0c 100644 --- a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParameterOrValueException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core.Exceptions; +namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; public class UnknownParameterOrValueException : CcuXmlRpcException { diff --git a/source/CreativeCoders.HomeMatic.Core/HomeMaticDeviceSystems.cs b/source/CreativeCoders.HomeMatic.XmlRpc/HomeMaticDeviceSystems.cs similarity index 77% rename from source/CreativeCoders.HomeMatic.Core/HomeMaticDeviceSystems.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/HomeMaticDeviceSystems.cs index 0d6dbff..e32c93d 100644 --- a/source/CreativeCoders.HomeMatic.Core/HomeMaticDeviceSystems.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/HomeMaticDeviceSystems.cs @@ -1,6 +1,6 @@ using System; -namespace CreativeCoders.HomeMatic.Core; +namespace CreativeCoders.HomeMatic.XmlRpc; [Flags] public enum HomeMaticDeviceSystems diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/ParameterDescription.cs b/source/CreativeCoders.HomeMatic.XmlRpc/ParameterDescription.cs index f7e0ff9..cf23c1d 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/ParameterDescription.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/ParameterDescription.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using CreativeCoders.HomeMatic.Core.Parameters; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; using CreativeCoders.HomeMatic.XmlRpc.Converters; using CreativeCoders.Net.XmlRpc.Definition; using JetBrains.Annotations; diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterDataType.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs similarity index 74% rename from source/CreativeCoders.HomeMatic.Core/Parameters/ParameterDataType.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs index 84d5ea0..5825820 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterDataType.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace CreativeCoders.HomeMatic.Core.Parameters; +namespace CreativeCoders.HomeMatic.XmlRpc.Parameters; [PublicAPI] public enum ParameterDataType diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/RxMode.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs similarity index 77% rename from source/CreativeCoders.HomeMatic.Core/Parameters/RxMode.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs index 1a6ad41..4c3a530 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/RxMode.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs @@ -1,7 +1,7 @@ using System; using JetBrains.Annotations; -namespace CreativeCoders.HomeMatic.Core.Parameters; +namespace CreativeCoders.HomeMatic.XmlRpc.Parameters; [PublicAPI] [Flags] diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs b/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs index 1deb6ce..f70508c 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs @@ -1,6 +1,6 @@ using System; using CreativeCoders.Core; -using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.XmlRpc; namespace CreativeCoders.HomeMatic.XmlRpc; diff --git a/source/CreativeCoders.HomeMatic/CcuDevice.cs b/source/CreativeCoders.HomeMatic/CcuDevice.cs index e1024f4..e690de7 100644 --- a/source/CreativeCoders.HomeMatic/CcuDevice.cs +++ b/source/CreativeCoders.HomeMatic/CcuDevice.cs @@ -1,6 +1,7 @@ using CreativeCoders.HomeMatic.Core.Devices; -using CreativeCoders.HomeMatic.Core.Parameters; using CreativeCoders.HomeMatic.XmlRpc.Client; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; namespace CreativeCoders.HomeMatic; diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs index 8ebe277..1d5e673 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs @@ -2,8 +2,9 @@ using System.Reflection; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Core.Devices; -using CreativeCoders.HomeMatic.Core.Parameters; using CreativeCoders.HomeMatic.XmlRpc; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; using CreativeCoders.HomeMatic.XmlRpc.Client; #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs index f2beb9a..29e3c4a 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs @@ -1,5 +1,6 @@ using CreativeCoders.HomeMatic.Core.Devices; using CreativeCoders.HomeMatic.XmlRpc.Client; +using CreativeCoders.HomeMatic.XmlRpc.Devices; namespace CreativeCoders.HomeMatic; diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs index 16ad6a6..e6163cd 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs @@ -1,6 +1,7 @@ using CreativeCoders.Core; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Tools.Cli.Base.SharedData; +using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Client; namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding; diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs index 5d3f21f..923d5d6 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs @@ -1,6 +1,7 @@ using System.Net; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.JsonRpc; +using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Client; using FakeItEasy; using AwesomeAssertions; diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs index 64089b4..0578b8f 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs @@ -1,8 +1,9 @@ using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Core.Devices; -using CreativeCoders.HomeMatic.Core.Parameters; using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Client; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; using FakeItEasy; using AwesomeAssertions; diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs index a111f4e..e333e53 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs @@ -1,8 +1,10 @@ using System.Text.Json; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Core.Devices; -using CreativeCoders.HomeMatic.Core.Parameters; using CreativeCoders.HomeMatic.Exporting; +using CreativeCoders.HomeMatic.XmlRpc; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; using FakeItEasy; using AwesomeAssertions; From e6b5e057b3df04c63b36724f1a1c6fc8703d9c46 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 15:55:14 +0200 Subject: [PATCH 03/15] chore(project): fix inconsistent formatting in package references in `JsonRpc.csproj` --- .../CreativeCoders.HomeMatic.JsonRpc.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/CreativeCoders.HomeMatic.JsonRpc/CreativeCoders.HomeMatic.JsonRpc.csproj b/source/CreativeCoders.HomeMatic.JsonRpc/CreativeCoders.HomeMatic.JsonRpc.csproj index 4e767b3..2167c99 100644 --- a/source/CreativeCoders.HomeMatic.JsonRpc/CreativeCoders.HomeMatic.JsonRpc.csproj +++ b/source/CreativeCoders.HomeMatic.JsonRpc/CreativeCoders.HomeMatic.JsonRpc.csproj @@ -9,7 +9,7 @@ - + From 5d34421e2e959cdf089ff4f52104342590680fd9 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:50:48 +0200 Subject: [PATCH 04/15] refactor(core): remove unused XML-RPC entities and enhance documentation - Removed obsolete classes: `CcuDeviceKindExtensions`, `HomeMaticDeviceSystems`, and `XmlRpcEndpoint`. - Added XML documentation to interfaces, enums, and exception classes across `CreativeCoders.HomeMatic.Core` and `CreativeCoders.HomeMatic.XmlRpc`. - Updated unit tests to reflect the removal of deprecated types and adjusted existing tests for consistency. --- .../CcuDeviceKind.cs | 9 ---- .../CcuDeviceKindExtensions.cs | 21 --------- .../CcuDeviceUri.cs | 30 ++++++++++++ .../CcuLogLevel.cs | 34 +++++++++++++- .../Devices/CcuParameterDescription.cs | 47 +++++++++++++++++++ .../Devices/ICcuDevice.cs | 7 +++ .../Devices/ICcuDeviceBase.cs | 24 ++++++++++ .../Devices/ICcuDeviceBaseData.cs | 31 ++++++++++++ .../Devices/ICcuDeviceChannel.cs | 3 ++ .../Devices/ICcuDeviceChannelData.cs | 15 ++++++ .../Devices/ICcuDeviceData.cs | 31 ++++++++++++ .../Devices/ICompleteCcuDevice.cs | 15 ++++++ .../Devices/ICompleteCcuDeviceChannel.cs | 11 +++++ .../IParamSetValuesWithDescriptions.cs | 11 +++++ .../Devices/ParamSetValue.cs | 11 +++++ .../Devices/ParamSetValueWithDescription.cs | 11 +++++ .../ICcuClient.cs | 21 +++++++++ .../ICcuClientFactory.cs | 13 +++++ .../ICompleteCcuDeviceBuilder.cs | 8 ++++ .../IMultiCcuClient.cs | 21 +++++++++ .../IMultiCcuClientFactory.cs | 18 +++++++ .../Parameters/ParamSetKey.cs | 27 ++++++++++- .../Parameters/ParameterFlags.cs | 33 ++++++++++++- .../Parameters/ParameterKind.cs | 26 +++++++++- .../CcuDeviceKind.cs | 27 +++++++++++ .../CcuRpcPorts.cs | 35 +++++++++++--- .../Devices/ChannelDirection.cs | 18 ++++++- .../Devices/DeviceFirmwareUpdateState.cs | 30 +++++++++++- .../Exceptions/CcuXmlRpcException.cs | 12 ++++- .../DeviceAddressExpectedException.cs | 12 ++++- .../Exceptions/DeviceOutOfReachException.cs | 12 ++++- .../Exceptions/GeneralXmlRpcException.cs | 12 ++++- .../Exceptions/HomeMaticException.cs | 12 ++++- .../InterfaceUpdateFailedException.cs | 12 ++++- .../Exceptions/NotEnoughDutyCycleException.cs | 12 ++++- .../OperationNotSupportedException.cs | 12 ++++- .../TransmissionOutstandingException.cs | 12 ++++- .../UnknownDeviceOrChannelException.cs | 12 ++++- .../Exceptions/UnknownParamSetException.cs | 12 ++++- .../UnknownParameterOrValueException.cs | 12 ++++- .../HomeMaticDeviceSystems.cs | 12 ----- .../Parameters/ParameterDataType.cs | 34 +++++++++++++- .../Parameters/RxMode.cs | 33 ++++++++++++- .../XmlRpcApiAddress.cs | 31 ++++++------ source/CreativeCoders.HomeMatic/CcuClient.cs | 4 +- .../CcuClientFactory.cs | 9 ++-- .../MultiCcuClientFactory.cs | 1 + .../XmlRpcApiConnection.cs | 5 +- .../XmlRpcEndpoint.cs | 27 ----------- .../Connections/CliHomeMaticClientBuilder.cs | 1 + .../CcuClientTests.cs | 4 +- 51 files changed, 752 insertions(+), 141 deletions(-) delete mode 100644 source/CreativeCoders.HomeMatic.Core/CcuDeviceKind.cs delete mode 100644 source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs create mode 100644 source/CreativeCoders.HomeMatic.XmlRpc/CcuDeviceKind.cs delete mode 100644 source/CreativeCoders.HomeMatic.XmlRpc/HomeMaticDeviceSystems.cs delete mode 100644 source/CreativeCoders.HomeMatic/XmlRpcEndpoint.cs diff --git a/source/CreativeCoders.HomeMatic.Core/CcuDeviceKind.cs b/source/CreativeCoders.HomeMatic.Core/CcuDeviceKind.cs deleted file mode 100644 index 59ed6ac..0000000 --- a/source/CreativeCoders.HomeMatic.Core/CcuDeviceKind.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CreativeCoders.HomeMatic.Core; - -public enum CcuDeviceKind -{ - HomeMatic, - HomeMaticIp, - HomeMaticWired, - Coupled -} diff --git a/source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs b/source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs deleted file mode 100644 index 0482f0a..0000000 --- a/source/CreativeCoders.HomeMatic.Core/CcuDeviceKindExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using CreativeCoders.HomeMatic.XmlRpc; -using JetBrains.Annotations; - -namespace CreativeCoders.HomeMatic.Core; - -[PublicAPI] -public static class CcuDeviceKindExtensions -{ - public static int ToPort(this CcuDeviceKind deviceKind) - { - return deviceKind switch - { - CcuDeviceKind.HomeMatic => CcuRpcPorts.HomeMatic, - CcuDeviceKind.HomeMaticIp => CcuRpcPorts.HomeMaticIp, - CcuDeviceKind.HomeMaticWired => CcuRpcPorts.HomeMaticWired, - CcuDeviceKind.Coupled => CcuRpcPorts.CoupledDevices, - _ => throw new ArgumentOutOfRangeException(nameof(deviceKind), deviceKind, null) - }; - } -} diff --git a/source/CreativeCoders.HomeMatic.Core/CcuDeviceUri.cs b/source/CreativeCoders.HomeMatic.Core/CcuDeviceUri.cs index 276b093..14058e1 100644 --- a/source/CreativeCoders.HomeMatic.Core/CcuDeviceUri.cs +++ b/source/CreativeCoders.HomeMatic.Core/CcuDeviceUri.cs @@ -1,20 +1,50 @@ using System.Diagnostics.CodeAnalysis; +using CreativeCoders.HomeMatic.XmlRpc; namespace CreativeCoders.HomeMatic.Core; +/// +/// Represents a URI that uniquely identifies a device on a specific CCU. +/// [ExcludeFromCodeCoverage] public class CcuDeviceUri { + /// + /// Gets the host name or IP address of the CCU. + /// + /// The network host name or IP address of the CCU. public required string CcuHost { get; init; } + /// + /// Gets the logical name of the CCU. + /// + /// The human-readable name of the CCU. The default is an empty string. public string CcuName { get; init; } = string.Empty; + /// + /// Gets the kind of device addressed by this URI. + /// + /// One of the enumeration values that specifies the device kind. public required CcuDeviceKind Kind { get; init; } + /// + /// Gets the device address within the CCU. + /// + /// The device or channel address. public required string Address { get; init; } + /// + /// Gets the preferred display name for the CCU host. + /// + /// + /// The value of if it is not empty; otherwise, the value of . + /// public string HostDisplayName => string.IsNullOrWhiteSpace(CcuName) ? CcuHost : CcuName; + /// + /// Returns a string representation of this URI in the form {Kind}://{CcuHost}/{Address}. + /// + /// A string representation of the device URI. public override string ToString() { return $"{Kind}://{CcuHost}/{Address}"; diff --git a/source/CreativeCoders.HomeMatic.Core/CcuLogLevel.cs b/source/CreativeCoders.HomeMatic.Core/CcuLogLevel.cs index b8b5ed2..b49cde0 100644 --- a/source/CreativeCoders.HomeMatic.Core/CcuLogLevel.cs +++ b/source/CreativeCoders.HomeMatic.Core/CcuLogLevel.cs @@ -1,15 +1,45 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core; +/// +/// Specifies the severity of log entries produced by the CCU. +/// [PublicAPI] public enum CcuLogLevel { + /// + /// All log entries are included. + /// All = 0, + + /// + /// Detailed diagnostic messages used for debugging. + /// Debug = 1, + + /// + /// Informational messages that describe normal operation. + /// Info = 2, + + /// + /// Notable events that do not indicate a problem. + /// Notice = 3, + + /// + /// Conditions that may indicate a potential problem. + /// Warning = 4, + + /// + /// Errors that affect the current operation. + /// Error = 5, + + /// + /// Fatal errors that prevent further operation. + /// FatalError = 6 -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs b/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs index 2e19479..57f6509 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs @@ -3,27 +3,74 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Describes the metadata of a single HomeMatic parameter as reported by the CCU. +/// public class CcuParameterDescription { + /// + /// Gets the identifier of the parameter. + /// + /// The parameter identifier, or if not provided. public required string? Id { get; init; } + /// + /// Gets the default value of the parameter. + /// + /// The default value, or if not provided. public required object? DefaultValue { get; init; } + /// + /// Gets the minimum value allowed for the parameter. + /// + /// The minimum value, or if not provided. public required object? MinValue { get; init; } + /// + /// Gets the maximum value allowed for the parameter. + /// + /// The maximum value, or if not provided. public required object? MaxValue { get; init; } + /// + /// Gets the raw type string as reported by the CCU. + /// + /// The type string, or if not provided. public required string? Type { get; init; } + /// + /// Gets the strongly-typed data type of the parameter. + /// + /// One of the enumeration values that specifies the data type. public required ParameterDataType DataType { get; init; } + /// + /// Gets the unit string of the parameter value. + /// + /// The unit string, or if not provided. public required string? Unit { get; init; } + /// + /// Gets the tab order used when displaying the parameter in the CCU UI. + /// + /// The tab-order index. public required int TabOrder { get; init; } + /// + /// Gets the control hint used for the parameter in the CCU UI. + /// + /// The control hint string, or if not provided. public required string? Control { get; init; } + /// + /// Gets the list of allowed enum value names when the parameter is of enum type. + /// + /// The enumerable of enum value names. public required IEnumerable ValuesList { get; init; } = []; + /// + /// Gets the special values associated with the parameter (for example, error or invalid markers). + /// + /// The enumerable of special-value dictionaries keyed by name. public required IEnumerable> SpecialValues { get; init; } = []; } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDevice.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDevice.cs index 807cb99..423136b 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDevice.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDevice.cs @@ -2,7 +2,14 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Represents a HomeMatic device including its channels and runtime state. +/// public interface ICcuDevice : ICcuDeviceBase, ICcuDeviceData { + /// + /// Gets the channels that belong to this device. + /// + /// The enumerable of instances. IEnumerable Channels { get; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBase.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBase.cs index 7ba331d..73d35ea 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBase.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBase.cs @@ -3,16 +3,40 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Defines the shared functionality for devices and channels, including access to their parameter sets. +/// public interface ICcuDeviceBase : ICcuDeviceBaseData { + /// + /// Asynchronously retrieves the current values for the specified parameter set. + /// + /// The key of the parameter set to read. + /// A task that yields the enumerable of entries in the parameter set. Task> GetParamSetValuesAsync(string paramSetKey); + /// + /// Asynchronously retrieves the descriptions for the specified parameter set. + /// + /// The key of the parameter set whose descriptions should be read. + /// A task that yields a grouping. Task GetParamSetDescriptionsAsync(string paramSetKey); } +/// +/// Groups the parameter descriptions that belong to a single parameter set. +/// public class CcuParameterDescriptions { + /// + /// Gets the key of the parameter set these descriptions belong to. + /// + /// The parameter-set key. public required string ParamSetKey { get; init; } + /// + /// Gets the parameter descriptions contained in this set. + /// + /// The enumerable of entries. public required IEnumerable Items { get; init; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs index 814bb07..9816386 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs @@ -1,18 +1,49 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Defines the common identifying and structural data of a HomeMatic device or channel. +/// public interface ICcuDeviceBaseData { + /// + /// Gets the URI that uniquely identifies the device or channel on its CCU. + /// + /// The of the device or channel. CcuDeviceUri Uri { get; } + /// + /// Gets the device type identifier as reported by the CCU. + /// + /// The device type string. string DeviceType { get; } + /// + /// Gets a value that indicates whether AES signing is active for this device or channel. + /// + /// if AES signing is active; otherwise, . bool IsAesActive { get; } + /// + /// Gets the interface identifier the device is attached to. + /// + /// The interface identifier. string Interface { get; } + /// + /// Gets the version of the device description. + /// + /// The version number. int Version { get; } + /// + /// Gets a value that indicates whether the device supports interface roaming. + /// + /// if the device supports roaming; otherwise, . bool Roaming { get; } + /// + /// Gets the parameter-set keys available for this device or channel. + /// + /// The array of parameter-set keys. string[] ParamSets { get; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs index b9c7303..7256f75 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs @@ -1,5 +1,8 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Represents a single channel of a HomeMatic device. +/// public interface ICcuDeviceChannel : ICcuDeviceBase, ICcuDeviceChannelData { } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs index 114d045..981a7c0 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs @@ -2,11 +2,26 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Defines the channel-specific data of a HomeMatic device channel. +/// public interface ICcuDeviceChannelData : ICcuDeviceBaseData { + /// + /// Gets the zero-based index of the channel within its parent device. + /// + /// The channel index. int Index { get; } + /// + /// Gets the paired channel address when the channel belongs to a button group. + /// + /// The address of the paired channel, or an empty string if none. string Group { get; } + /// + /// Gets the direction of the channel in a direct device link. + /// + /// One of the enumeration values that specifies the channel direction. ChannelDirection ChannelDirection { get; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs index 01d09bd..824e30a 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs @@ -4,19 +4,50 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Defines the device-level data of a HomeMatic device. +/// public interface ICcuDeviceData : ICcuDeviceBaseData { + /// + /// Gets the human-readable name of the device. + /// + /// The device name. string Name { get; } + /// + /// Gets the reception mode flags for the device. + /// + /// A bitwise combination of the enumeration values that specifies the reception mode. RxMode RxMode { get; } + /// + /// Gets the RF address of the device on the BidCoS radio bus. + /// + /// The numeric RF address. int RfAddress { get; } + /// + /// Gets the currently installed firmware version. + /// + /// The firmware version string. string Firmware { get; } + /// + /// Gets the firmware version available for update. + /// + /// The available firmware version string, or empty if no update is available. string AvailableFirmware { get; } + /// + /// Gets a value that indicates whether the device firmware can be updated. + /// + /// if the device supports firmware updates; otherwise, . bool CanBeUpdated { get; } + /// + /// Gets the current firmware update state of the device. + /// + /// One of the enumeration values that specifies the firmware update state. DeviceFirmwareUpdateState FirmwareUpdateState { get; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDevice.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDevice.cs index 987af3d..98345d7 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDevice.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDevice.cs @@ -2,11 +2,26 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Represents a HomeMatic device combined with all its parameter-set values and descriptions. +/// public interface ICompleteCcuDevice { + /// + /// Gets the device-level data. + /// + /// The for this device. ICcuDeviceData DeviceData { get; } + /// + /// Gets the channels of the device with their parameter-set values and descriptions. + /// + /// The enumerable of instances. IEnumerable Channels { get; } + /// + /// Gets the parameter-set values and descriptions for the device itself. + /// + /// The enumerable of groups. IEnumerable ParamSetValues { get; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDeviceChannel.cs index b740e65..685703c 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDeviceChannel.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICompleteCcuDeviceChannel.cs @@ -2,9 +2,20 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Represents a channel combined with all its parameter-set values and descriptions. +/// public interface ICompleteCcuDeviceChannel { + /// + /// Gets the channel-level data. + /// + /// The for this channel. ICcuDeviceChannelData ChannelData { get; } + /// + /// Gets the parameter-set values and descriptions for the channel. + /// + /// The enumerable of groups. IEnumerable ParamSetValues { get; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/IParamSetValuesWithDescriptions.cs b/source/CreativeCoders.HomeMatic.Core/Devices/IParamSetValuesWithDescriptions.cs index 007dfff..0e2c190 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/IParamSetValuesWithDescriptions.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/IParamSetValuesWithDescriptions.cs @@ -2,9 +2,20 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Groups the values and descriptions that belong to a single parameter set. +/// public class ParamSetValuesWithDescriptions { + /// + /// Gets the key of the parameter set. + /// + /// The parameter-set key. public required string ParamSetKey { get; init; } + /// + /// Gets the parameter values along with their descriptions. + /// + /// The enumerable of entries. public required IEnumerable ParamSetValues { get; init; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValue.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValue.cs index 0b1ecda..dad49d2 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValue.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValue.cs @@ -1,8 +1,19 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Represents a single named parameter value within a parameter set. +/// public class ParamSetValue { + /// + /// Gets the name of the parameter. + /// + /// The parameter name. public required string Name { get; init; } + /// + /// Gets the current value of the parameter. + /// + /// The parameter value. public required object Value { get; init; } } diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValueWithDescription.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValueWithDescription.cs index 0b6c4ad..b744cdc 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValueWithDescription.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ParamSetValueWithDescription.cs @@ -1,8 +1,19 @@ namespace CreativeCoders.HomeMatic.Core.Devices; +/// +/// Pairs a parameter value with its corresponding description. +/// public class ParamSetValueWithDescription { + /// + /// Gets the parameter value. + /// + /// The instance. public required ParamSetValue ParamSetValue { get; init; } + /// + /// Gets the description that belongs to the parameter. + /// + /// The instance. public required CcuParameterDescription Description { get; init; } } diff --git a/source/CreativeCoders.HomeMatic.Core/ICcuClient.cs b/source/CreativeCoders.HomeMatic.Core/ICcuClient.cs index 4e11c41..3ea57e4 100644 --- a/source/CreativeCoders.HomeMatic.Core/ICcuClient.cs +++ b/source/CreativeCoders.HomeMatic.Core/ICcuClient.cs @@ -4,13 +4,34 @@ namespace CreativeCoders.HomeMatic.Core; +/// +/// Provides access to the devices of a single HomeMatic CCU. +/// public interface ICcuClient { + /// + /// Asynchronously retrieves all devices known to the CCU. + /// + /// A task that yields an enumerable of instances. Task> GetDevicesAsync(); + /// + /// Asynchronously retrieves a single device by its address. + /// + /// The device address. + /// A task that yields the matching . Task GetDeviceAsync(string address); + /// + /// Asynchronously retrieves all devices including their parameter descriptions. + /// + /// A task that yields an enumerable of instances. Task> GetCompleteDevicesAsync(); + /// + /// Asynchronously retrieves a single device including its parameter descriptions. + /// + /// The device address. + /// A task that yields the matching . Task GetCompleteDeviceAsync(string address); } diff --git a/source/CreativeCoders.HomeMatic.Core/ICcuClientFactory.cs b/source/CreativeCoders.HomeMatic.Core/ICcuClientFactory.cs index 2bdbdbe..2215422 100644 --- a/source/CreativeCoders.HomeMatic.Core/ICcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic.Core/ICcuClientFactory.cs @@ -1,9 +1,22 @@ using System.Collections.Generic; +using CreativeCoders.HomeMatic.XmlRpc; namespace CreativeCoders.HomeMatic.Core; +/// +/// Creates instances configured for a specific CCU. +/// public interface ICcuClientFactory { + /// + /// Creates a new that connects to the specified CCU. + /// + /// The logical name of the CCU. + /// The device kinds the client should address. + /// The host name or IP address of the CCU. + /// The user name used to authenticate against the CCU. + /// The password used to authenticate against the CCU. + /// A new instance. ICcuClient CreateClient(string ccuName, IEnumerable deviceKinds, string host, string userName, string password); } diff --git a/source/CreativeCoders.HomeMatic.Core/ICompleteCcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic.Core/ICompleteCcuDeviceBuilder.cs index 6efed59..9fca400 100644 --- a/source/CreativeCoders.HomeMatic.Core/ICompleteCcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic.Core/ICompleteCcuDeviceBuilder.cs @@ -3,7 +3,15 @@ namespace CreativeCoders.HomeMatic.Core; +/// +/// Builds instances from basic data by loading parameter descriptions. +/// public interface ICompleteCcuDeviceBuilder { + /// + /// Asynchronously builds a complete device representation for the specified device. + /// + /// The base device to augment with parameter descriptions. + /// A task that yields the completed . Task BuildAsync(ICcuDevice device); } diff --git a/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs b/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs index 052a22b..1a6a294 100644 --- a/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs +++ b/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs @@ -4,13 +4,34 @@ namespace CreativeCoders.HomeMatic.Core; +/// +/// Provides unified access to devices across multiple HomeMatic CCUs. +/// public interface IMultiCcuClient { + /// + /// Asynchronously retrieves all devices from every configured CCU. + /// + /// A task that yields an enumerable of instances. Task> GetDevicesAsync(); + /// + /// Asynchronously retrieves a single device by its address across all configured CCUs. + /// + /// The device address. + /// A task that yields the matching . Task GetDeviceAsync(string address); + /// + /// Asynchronously retrieves all devices including their parameter descriptions from every configured CCU. + /// + /// A task that yields an enumerable of instances. Task> GetCompleteDevicesAsync(); + /// + /// Asynchronously retrieves a single device including its parameter descriptions across all configured CCUs. + /// + /// The device address. + /// A task that yields the matching . Task GetCompleteDeviceAsync(string address); } diff --git a/source/CreativeCoders.HomeMatic.Core/IMultiCcuClientFactory.cs b/source/CreativeCoders.HomeMatic.Core/IMultiCcuClientFactory.cs index 9081484..bd0bb64 100644 --- a/source/CreativeCoders.HomeMatic.Core/IMultiCcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic.Core/IMultiCcuClientFactory.cs @@ -1,9 +1,27 @@ +using CreativeCoders.HomeMatic.XmlRpc; + namespace CreativeCoders.HomeMatic.Core; +/// +/// Builds an by aggregating one or more individual CCU configurations. +/// public interface IMultiCcuClientFactory { + /// + /// Adds a CCU to the builder configuration. + /// + /// The logical name of the CCU. + /// The host name or IP address of the CCU. + /// The user name used to authenticate against the CCU. + /// The password used to authenticate against the CCU. + /// The device kinds the CCU should serve. + /// The same instance, to allow chaining calls. IMultiCcuClientFactory AddCcu(string ccuName, string host, string userName, string password, params CcuDeviceKind[] deviceKinds); + /// + /// Builds an from the previously added CCU configurations. + /// + /// A new instance. IMultiCcuClient Build(); } diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs index 25c94d3..9d5967b 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs +++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs @@ -1,20 +1,43 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core.Parameters; +/// +/// Defines the well-known parameter-set key names used by the CCU. +/// [PublicAPI] public class ParamSetKey { + /// + /// The parameter set containing master (configuration) parameters. + /// public const string Master = "MASTER"; + /// + /// The parameter set containing value (state) parameters. + /// public const string Values = "VALUES"; + /// + /// The parameter set containing link (direct device link) parameters. + /// public const string Link = "LINK"; + /// + /// The parameter set containing service parameters. + /// public const string Service = "SERVICE"; + /// + /// All known parameter-set keys. + /// public static readonly string[] ParamSetKeys = {Master, Values, Link, Service}; + /// + /// Converts a parameter-set key string into the corresponding value. + /// + /// The parameter-set key name. The comparison is case-insensitive. + /// The matching , or if no match is found. public static ParameterKind StringToParameterKind(string text) { return text.ToUpper() switch @@ -26,4 +49,4 @@ public static ParameterKind StringToParameterKind(string text) _ => ParameterKind.Undefined }; } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs index c67bd23..1d20c69 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs +++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs @@ -1,16 +1,45 @@ -using System; +using System; using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core.Parameters; +/// +/// Specifies attribute flags for a HomeMatic parameter. +/// +/// +/// Values can be combined with bitwise OR. +/// [PublicAPI] [Flags] public enum ParameterFlags { + /// + /// No flags are set. + /// None = 0, + + /// + /// The parameter is visible to end users in the CCU UI. + /// Visible = 1, + + /// + /// The parameter is used internally and is not meant to be shown to end users. + /// Internal = 2, + + /// + /// The parameter requires a value transformation before use. + /// Transform = 4, + + /// + /// The parameter represents a service message or diagnostics value. + /// Service = 8, + + /// + /// The parameter is sticky — it must be acknowledged explicitly to be reset. + /// Sticky = 16 -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterKind.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterKind.cs index 6efdd3a..7bb72d1 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterKind.cs +++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterKind.cs @@ -1,10 +1,32 @@ -namespace CreativeCoders.HomeMatic.Core.Parameters; +namespace CreativeCoders.HomeMatic.Core.Parameters; +/// +/// Specifies the kind of parameter set that a parameter belongs to. +/// public enum ParameterKind { + /// + /// The parameter kind is not defined. + /// Undefined, + + /// + /// The parameter belongs to the master (configuration) parameter set. + /// Master, + + /// + /// The parameter belongs to the values (state) parameter set. + /// Values, + + /// + /// The parameter belongs to the link (direct device link) parameter set. + /// Link, + + /// + /// The parameter belongs to the service parameter set. + /// Service -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/CcuDeviceKind.cs b/source/CreativeCoders.HomeMatic.XmlRpc/CcuDeviceKind.cs new file mode 100644 index 0000000..05a3c9b --- /dev/null +++ b/source/CreativeCoders.HomeMatic.XmlRpc/CcuDeviceKind.cs @@ -0,0 +1,27 @@ +namespace CreativeCoders.HomeMatic.XmlRpc; + +/// +/// Specifies the kind of HomeMatic device addressed on a CCU. +/// +public enum CcuDeviceKind +{ + /// + /// A classic BidCoS-RF (HomeMatic) device. + /// + HomeMatic, + + /// + /// A HomeMatic IP device. + /// + HomeMaticIp, + + /// + /// A HomeMatic Wired (RS485) device. + /// + HomeMaticWired, + + /// + /// A device that is coupled across multiple interfaces. + /// + Coupled +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs b/source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs index e5a13f7..435299f 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs @@ -3,26 +3,47 @@ namespace CreativeCoders.HomeMatic.XmlRpc; +/// +/// Provides the well-known TCP port numbers used by the CCU XML-RPC interfaces. +/// [PublicAPI] public static class CcuRpcPorts { + /// + /// The XML-RPC port for coupled (multi-system) devices. + /// public const int CoupledDevices = 9292; + /// + /// The XML-RPC port for the classic BidCoS-RF HomeMatic system. + /// public const int HomeMatic = 2001; + /// + /// The XML-RPC port for the HomeMatic IP system. + /// public const int HomeMaticIp = 2010; + /// + /// The XML-RPC port for the HomeMatic Wired (RS485) system. + /// public const int HomeMaticWired = 2000; - public static int ToPort(this HomeMaticDeviceSystems deviceSystems) + /// + /// Returns the XML-RPC port number used by the CCU for the specified device kind. + /// + /// One of the enumeration values that specifies the device kind. + /// The TCP port number used by the CCU XML-RPC endpoint for . + /// is not a defined value of . + public static int ToPort(this CcuDeviceKind deviceKind) { - return deviceSystems switch + return deviceKind switch { - HomeMaticDeviceSystems.HomeMatic => HomeMatic, - HomeMaticDeviceSystems.HomeMaticIp => HomeMaticIp, - HomeMaticDeviceSystems.HomeMaticWired => HomeMaticWired, - HomeMaticDeviceSystems.CoupledDevice => CoupledDevices, - _ => throw new ArgumentOutOfRangeException(nameof(deviceSystems), deviceSystems, null) + CcuDeviceKind.HomeMatic => HomeMatic, + CcuDeviceKind.HomeMaticIp => HomeMaticIp, + CcuDeviceKind.HomeMaticWired => HomeMaticWired, + CcuDeviceKind.Coupled => CoupledDevices, + _ => throw new ArgumentOutOfRangeException(nameof(deviceKind), deviceKind, null) }; } } diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs index 1ac2b3b..8070634 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs @@ -1,11 +1,25 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.XmlRpc.Devices; +/// +/// Specifies the direction of a channel in a direct device link. +/// [PublicAPI] public enum ChannelDirection { + /// + /// The channel has no defined direction. + /// None = 0, + + /// + /// The channel acts as a sender in direct device links. + /// Sender = 1, + + /// + /// The channel acts as a receiver in direct device links. + /// Receiver = 2 -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs index 86339c9..2a3f852 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs @@ -1,14 +1,40 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.XmlRpc.Devices; +/// +/// Specifies the firmware update state of a device. +/// [PublicAPI] public enum DeviceFirmwareUpdateState { + /// + /// No firmware update state is reported. + /// None, + + /// + /// The device firmware is up to date. + /// UpToDate, + + /// + /// A new firmware version is available for the device. + /// NewFirmwareAvailable, + + /// + /// The firmware image is being delivered to the device. + /// DeliverFirmwareImage, + + /// + /// The device is ready to perform the firmware update. + /// ReadyForUpdate, + + /// + /// The firmware update is currently being performed. + /// PerformingUpdate -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs index fa2800f..71608bc 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs @@ -1,12 +1,20 @@ -using System; +using System; using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// Serves as the base class for exceptions that wrap XML-RPC faults returned by the CCU. +/// [PublicAPI] public abstract class CcuXmlRpcException : HomeMaticException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. protected CcuXmlRpcException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs index 20a44e9..2fc0571 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when a CCU operation expected a device address but received a different value. +/// public class DeviceAddressExpectedException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public DeviceAddressExpectedException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs index 852c869..159cfe9 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the target device could not be reached by the CCU. +/// public class DeviceOutOfReachException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public DeviceOutOfReachException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs index cd3b2ac..aeb44ca 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the CCU reports a general XML-RPC fault that does not match a more specific type. +/// public class GeneralException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public GeneralException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs index 14ffd4c..ce07018 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// Serves as the base class for all HomeMatic-specific exceptions. +/// public abstract class HomeMaticException : Exception { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. protected HomeMaticException(string message, Exception innerException) : base(message, innerException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs index 4d1d0ed..8803ac0 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the CCU failed to update an interface. +/// public class InterfaceUpdateFailedException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public InterfaceUpdateFailedException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs index e137fb7..09c94a0 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the CCU cannot execute an operation because the available radio duty cycle is exhausted. +/// public class NotEnoughDutyCycleException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public NotEnoughDutyCycleException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs index e6c7a31..42733ac 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the requested operation is not supported by the CCU or the target device. +/// public class OperationNotSupportedException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public OperationNotSupportedException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs index 8d9c551..e336c67 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when a previous transmission to the target device is still outstanding. +/// public class TransmissionOutstandingException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public TransmissionOutstandingException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs index 72f2f21..43502a7 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the CCU does not recognize the requested device or channel. +/// public class UnknownDeviceOrChannelException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public UnknownDeviceOrChannelException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs index 73edf16..85163cb 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the CCU does not recognize the requested parameter set. +/// public class UnknownParamSetException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public UnknownParamSetException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs index f47ed0c..fc67396 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs @@ -1,10 +1,18 @@ -using System; +using System; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; +/// +/// The exception that is thrown when the CCU does not recognize the requested parameter or the provided value is invalid. +/// public class UnknownParameterOrValueException : CcuXmlRpcException { + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The original XML-RPC fault exception. public UnknownParameterOrValueException(string message, Exception faultException) : base(message, faultException) { } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/HomeMaticDeviceSystems.cs b/source/CreativeCoders.HomeMatic.XmlRpc/HomeMaticDeviceSystems.cs deleted file mode 100644 index e32c93d..0000000 --- a/source/CreativeCoders.HomeMatic.XmlRpc/HomeMaticDeviceSystems.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace CreativeCoders.HomeMatic.XmlRpc; - -[Flags] -public enum HomeMaticDeviceSystems -{ - HomeMatic = 1, - HomeMaticIp = 2, - HomeMaticWired = 4, - CoupledDevice = 8 -} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs index 5825820..367be27 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs @@ -1,15 +1,45 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.XmlRpc.Parameters; +/// +/// Specifies the data type of a HomeMatic parameter value. +/// [PublicAPI] public enum ParameterDataType { + /// + /// The data type is unknown or not yet determined. + /// Unknown, + + /// + /// A signed integer value. + /// Integer, + + /// + /// A boolean value. + /// Bool, + + /// + /// A floating-point value. + /// Float, + + /// + /// An action trigger value (write-only, with no meaningful stored state). + /// Action, + + /// + /// An enumeration value represented as an integer index. + /// Enum, + + /// + /// A textual value. + /// String -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs index 4c3a530..d4e3182 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs @@ -1,16 +1,45 @@ -using System; +using System; using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.XmlRpc.Parameters; +/// +/// Specifies the reception mode of a BidCoS-RF device. +/// +/// +/// Values can be combined with bitwise OR. +/// [PublicAPI] [Flags] public enum RxMode { + /// + /// No reception mode is set. + /// None = 0, + + /// + /// The device is always listening for incoming messages. + /// Always = 1, + + /// + /// The device listens only after a wake-up burst. + /// Burst = 2, + + /// + /// The device is in configuration reception mode. + /// Config = 4, + + /// + /// The device wakes up periodically to receive messages. + /// WakeUp = 8, + + /// + /// The device uses a lazy configuration mode to save battery. + /// LazyConfig = 16 -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs b/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs index f70508c..6f76eb2 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs @@ -1,16 +1,15 @@ -using System; +using System; using CreativeCoders.Core; -using CreativeCoders.HomeMatic.XmlRpc; namespace CreativeCoders.HomeMatic.XmlRpc; /// -/// Combines a CCU base URL with a HomeMatic device system to produce the final XML-RPC API endpoint URL. +/// Combines a CCU base URL with a HomeMatic device kind to produce the final XML-RPC API endpoint URL. /// /// -/// The HomeMatic CCU exposes separate XML-RPC servers on different ports for each device system. +/// The HomeMatic CCU exposes separate XML-RPC servers on different ports for each device kind. /// BidCoS-RF (wireless) uses port 2001, HM-IP uses port 2010 and BidCoS-Wired uses port 2000. -/// This class derives the correct port from the value via +/// This class derives the correct port from the value via /// . /// public class XmlRpcApiAddress @@ -19,21 +18,23 @@ public class XmlRpcApiAddress /// Initializes a new instance of the class. /// /// The base URL of the HomeMatic CCU (host and scheme, without port). - /// One of the enumeration values that specifies the target HomeMatic device system. - public XmlRpcApiAddress(Uri baseUrl, HomeMaticDeviceSystems deviceSystems) + /// One of the enumeration values that specifies the target HomeMatic device kind. + public XmlRpcApiAddress(Uri baseUrl, CcuDeviceKind deviceKind) { BaseUrl = Ensure.NotNull(baseUrl); - DeviceSystems = deviceSystems; + DeviceKind = deviceKind; } /// - /// Builds the final XML-RPC API URL by combining with the port derived from . + /// Builds the final XML-RPC API URL by combining with the port derived from . /// - /// The complete URL to the XML-RPC endpoint for the configured device system. + /// The complete URL to the XML-RPC endpoint for the configured device kind. public Uri ToApiUrl() { - var uriBuilder = new UriBuilder(BaseUrl); - uriBuilder.Port = DeviceSystems.ToPort(); + var uriBuilder = new UriBuilder(BaseUrl) + { + Port = DeviceKind.ToPort() + }; return uriBuilder.Uri; } @@ -45,8 +46,8 @@ public Uri ToApiUrl() public Uri BaseUrl { get; } /// - /// Gets the HomeMatic device system that determines the target XML-RPC port. + /// Gets the HomeMatic device kind that determines the target XML-RPC port. /// - /// One of the values. - public HomeMaticDeviceSystems DeviceSystems { get; } + /// One of the values. + public CcuDeviceKind DeviceKind { get; } } diff --git a/source/CreativeCoders.HomeMatic/CcuClient.cs b/source/CreativeCoders.HomeMatic/CcuClient.cs index edc2045..30c15bf 100644 --- a/source/CreativeCoders.HomeMatic/CcuClient.cs +++ b/source/CreativeCoders.HomeMatic/CcuClient.cs @@ -48,10 +48,10 @@ private CcuDevice CreateDevice(DeviceDescription deviceDescription, XmlRpcApiCon .WithApi(xmlRpcApiConnection.Api) .WithUri(new CcuDeviceUri { - CcuHost = xmlRpcApiConnection.Endpoint.BaseUrl.Host, + CcuHost = xmlRpcApiConnection.Address.BaseUrl.Host, CcuName = xmlRpcApiConnection.CcuName, Address = deviceDescription.Address, - Kind = xmlRpcApiConnection.Endpoint.DeviceKind + Kind = xmlRpcApiConnection.Address.DeviceKind }) .WithAllDevices(allDevices) .Build(); diff --git a/source/CreativeCoders.HomeMatic/CcuClientFactory.cs b/source/CreativeCoders.HomeMatic/CcuClientFactory.cs index dd2645b..673be6a 100644 --- a/source/CreativeCoders.HomeMatic/CcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic/CcuClientFactory.cs @@ -2,6 +2,7 @@ using CreativeCoders.Core.Collections; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.JsonRpc; +using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Client; using Microsoft.Extensions.DependencyInjection; @@ -33,8 +34,8 @@ private Dictionary CreateXmlRpcApis(IEnumera deviceKinds.ForEach(x => { - var xmlRpcEndpoint = new XmlRpcEndpoint(baseUrl.Uri, x); - xmlRpcApis[x] = new XmlRpcApiConnection(xmlRpcEndpoint, CreateXmlRpcApi(xmlRpcEndpoint)) + var apiAddress = new XmlRpcApiAddress(baseUrl.Uri, x); + xmlRpcApis[x] = new XmlRpcApiConnection(apiAddress, CreateXmlRpcApi(apiAddress)) { CcuName = ccuName }; @@ -43,10 +44,10 @@ private Dictionary CreateXmlRpcApis(IEnumera return xmlRpcApis; } - private IHomeMaticXmlRpcApi CreateXmlRpcApi(XmlRpcEndpoint xmlRpcEndpoint) + private IHomeMaticXmlRpcApi CreateXmlRpcApi(XmlRpcApiAddress apiAddress) { return xmlRpcApiBuilder - .ForUrl(xmlRpcEndpoint.ToApiUrl()) + .ForUrl(apiAddress.ToApiUrl()) .Build(); } diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs index 5075b7f..8464db7 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs @@ -1,4 +1,5 @@ using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.XmlRpc; namespace CreativeCoders.HomeMatic; diff --git a/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs b/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs index 2434630..b115737 100644 --- a/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs +++ b/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs @@ -1,11 +1,12 @@ using CreativeCoders.Core; +using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Client; namespace CreativeCoders.HomeMatic; -public class XmlRpcApiConnection(XmlRpcEndpoint endpoint, IHomeMaticXmlRpcApi api) +public class XmlRpcApiConnection(XmlRpcApiAddress address, IHomeMaticXmlRpcApi api) { - public XmlRpcEndpoint Endpoint { get; } = Ensure.NotNull(endpoint); + public XmlRpcApiAddress Address { get; } = Ensure.NotNull(address); public string CcuName { get; set; } = string.Empty; diff --git a/source/CreativeCoders.HomeMatic/XmlRpcEndpoint.cs b/source/CreativeCoders.HomeMatic/XmlRpcEndpoint.cs deleted file mode 100644 index 76a95db..0000000 --- a/source/CreativeCoders.HomeMatic/XmlRpcEndpoint.cs +++ /dev/null @@ -1,27 +0,0 @@ -using CreativeCoders.Core; -using CreativeCoders.HomeMatic.Core; - -namespace CreativeCoders.HomeMatic; - -public class XmlRpcEndpoint -{ - public XmlRpcEndpoint(Uri baseUrl, CcuDeviceKind deviceKind) - { - BaseUrl = Ensure.NotNull(baseUrl); - DeviceKind = deviceKind; - } - - public Uri ToApiUrl() - { - var uriBuilder = new UriBuilder(BaseUrl) - { - Port = DeviceKind.ToPort() - }; - - return uriBuilder.Uri; - } - - public Uri BaseUrl { get; } - - public CcuDeviceKind DeviceKind { get; } -} diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs index d95d103..51b878d 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs @@ -1,6 +1,7 @@ using CreativeCoders.Core; using CreativeCoders.Core.Collections; using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.XmlRpc; namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Connections; diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs index 7cffc3c..67f0c39 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs @@ -262,11 +262,11 @@ private static CcuClient CreateCcuClient(IHomeMaticJsonRpcClient jsonRpcClient, ICompleteCcuDeviceBuilder? completeCcuDeviceBuilder = null) { var homeMaticXmlRpcApiConnection = new XmlRpcApiConnection( - new XmlRpcEndpoint(new Uri("http://example.com"), CcuDeviceKind.HomeMatic), + new XmlRpcApiAddress(new Uri("http://example.com"), CcuDeviceKind.HomeMatic), homeMaticXmlRpcApi); var homeMaticIpXmlRpcApiConnection = new XmlRpcApiConnection( - new XmlRpcEndpoint(new Uri("http://example.com"), CcuDeviceKind.HomeMaticIp), + new XmlRpcApiAddress(new Uri("http://example.com"), CcuDeviceKind.HomeMaticIp), homeMaticIpXmlRpcApi); var xmlRpcApis = new Dictionary From 56eced00eeb636194cc3aee78aafa588c46a27a3 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:58:08 +0200 Subject: [PATCH 05/15] refactor(homeMatic): simplify `CcuDeviceBuilder` and make `ParamSetKey` static - Removed commented-out legacy code from `CcuDeviceBuilder`. - Updated `ParamSetKey` class to be static for improved clarity and usage. --- .../Parameters/ParamSetKey.cs | 2 +- .../CcuDeviceBuilder.cs | 112 +----------------- 2 files changed, 2 insertions(+), 112 deletions(-) diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs index 9d5967b..df19a4c 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs +++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs @@ -6,7 +6,7 @@ namespace CreativeCoders.HomeMatic.Core.Parameters; /// Defines the well-known parameter-set key names used by the CCU. /// [PublicAPI] -public class ParamSetKey +public static class ParamSetKey { /// /// The parameter set containing master (configuration) parameters. diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs index 1d5e673..cf1d495 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs @@ -7,121 +7,11 @@ using CreativeCoders.HomeMatic.XmlRpc.Parameters; using CreativeCoders.HomeMatic.XmlRpc.Client; -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - namespace CreativeCoders.HomeMatic; -// public class CcuDeviceBuilder -// { -// private CcuDeviceUri? _uri; -// -// private DeviceDescription? _deviceDescription; -// -// private IHomeMaticXmlRpcApi? _api; -// -// private IEnumerable? _devices; -// -// public CcuDeviceBuilder WithUri(CcuDeviceUri deviceUri) -// { -// _uri = deviceUri; -// -// var builder = new CcuDeviceBuilder -// { -// _api = _api, -// _deviceDescription = _deviceDescription, -// _uri = deviceUri, -// _devices = _devices -// }; -// -// return builder; -// } -// -// public CcuDeviceBuilder WithApi(IHomeMaticXmlRpcApi api) -// { -// _api = api; -// -// return this; -// } -// -// public CcuDeviceBuilder FromDeviceDescription(DeviceDescription deviceDescription) -// { -// _deviceDescription = deviceDescription; -// -// return this; -// } -// -// public CcuDevice Build() -// { -// if (_uri == null || _api == null || _devices == null) -// { -// throw new InvalidOperationException("Uri, Api and Devices must be set"); -// } -// -// var ccuDevice = new CcuDevice(_api) -// { -// Uri = _uri, -// DeviceType = _deviceDescription?.DeviceType ?? string.Empty, -// Version = _deviceDescription?.Version ?? 0, -// IsAesActive = _deviceDescription?.IsAesActive ?? false, -// Interface = _deviceDescription?.Interface ?? string.Empty, -// RxMode = _deviceDescription?.RxMode ?? RxMode.None, -// RfAddress = _deviceDescription?.RfAddress ?? 0, -// Firmware = _deviceDescription?.Firmware ?? string.Empty, -// AvailableFirmware = _deviceDescription?.AvailableFirmware ?? string.Empty, -// CanBeUpdated = _deviceDescription?.CanBeUpdated ?? false, -// FirmwareUpdateState = _deviceDescription?.FirmwareUpdateState ?? DeviceFirmwareUpdateState.None, -// Roaming = _deviceDescription?.Roaming ?? false, -// ParamSets = _deviceDescription?.ParamSets ?? [], -// Channels = CreateChannelsForDevice(_deviceDescription, _devices), -// }; -// -// return ccuDevice; -// } -// -// private IEnumerable CreateChannelsForDevice(DeviceDescription? deviceDescription, -// IEnumerable devices) -// { -// if (deviceDescription == null) -// { -// return []; -// } -// -// var channels = devices -// .Where(x => x.Parent?.Equals(deviceDescription.Address, StringComparison.OrdinalIgnoreCase) ?? false) -// .Select(x => new CcuDeviceChannel(_api!) -// { -// Uri = new CcuDeviceUri -// { -// CcuHost = _uri!.CcuHost, -// CcuName = _uri.CcuName, -// Address = x.Address, -// Kind = _uri.Kind -// }, -// DeviceType = x.DeviceType, -// Version = x.Version, -// IsAesActive = x.IsAesActive, -// Interface = x.Interface, -// Roaming = x.Roaming, -// ParamSets = x.ParamSets, -// Index = x.Index, -// Group = x.Group, -// ChannelDirection = x.ChannelDirection -// }) -// .OrderBy(x => x.Index); -// -// return [..channels]; -// } -// -// public CcuDeviceBuilder WithAllDevices(IEnumerable devices) -// { -// _devices = devices; -// -// return this; -// } -// } - public class CcuDeviceBuilder : ObjectBuilderBase { + #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value private CcuDeviceUri? _uri; private DeviceDescription? _deviceDescription; From 0f48e12729b09c40a3c7828314b3392cd6a332b4 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:24:17 +0200 Subject: [PATCH 06/15] refactor(core, xmlrpc): rename enums and properties for consistency - Renamed `RxMode` to `RxModes` across codebase to align with naming conventions. - Refactored `ParameterFlags` to `ParameterUiAttributes`. - Updated related tests and XML-RPC mappings to reflect the changes. - Included suppressions for improved static analysis compatibility in `CcuDeviceBuilder`. --- HomeMatic.sln | 5 +++++ .../Devices/ICcuDeviceData.cs | 2 +- .../{ParameterFlags.cs => ParameterUiAttributes.cs} | 2 +- .../DeviceDescription.cs | 12 +++++++----- .../Parameters/{RxMode.cs => RxModes.cs} | 2 +- source/CreativeCoders.HomeMatic/CcuDevice.cs | 2 +- source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs | 8 ++++++-- .../CcuDeviceBuilderTests.cs | 6 +++--- 8 files changed, 25 insertions(+), 14 deletions(-) rename source/CreativeCoders.HomeMatic.Core/Parameters/{ParameterFlags.cs => ParameterUiAttributes.cs} (96%) rename source/CreativeCoders.HomeMatic.XmlRpc/Parameters/{RxMode.cs => RxModes.cs} (97%) diff --git a/HomeMatic.sln b/HomeMatic.sln index 09122e0..cab3077 100644 --- a/HomeMatic.sln +++ b/HomeMatic.sln @@ -74,6 +74,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.HomeMatic.To EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.HomeMatic.Tools.Cli.Commands", "source\Tools\Cli\CreativeCoders.HomeMatic.Tools.Cli.Commands\CreativeCoders.HomeMatic.Tools.Cli.Commands.csproj", "{822ECD72-5DB0-4637-B794-CE27B02827AC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__docs", "__docs", "{24644358-1979-4A95-A64C-C686A71E2AF1}" + ProjectSection(SolutionItems) = preProject + docs\HomeMatic-XmlRpc.md = docs\HomeMatic-XmlRpc.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs index 824e30a..d18a4ad 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs @@ -19,7 +19,7 @@ public interface ICcuDeviceData : ICcuDeviceBaseData /// Gets the reception mode flags for the device. /// /// A bitwise combination of the enumeration values that specifies the reception mode. - RxMode RxMode { get; } + RxModes RxMode { get; } /// /// Gets the RF address of the device on the BidCoS radio bus. diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterUiAttributes.cs similarity index 96% rename from source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs rename to source/CreativeCoders.HomeMatic.Core/Parameters/ParameterUiAttributes.cs index 1d20c69..556a8ef 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs +++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterUiAttributes.cs @@ -11,7 +11,7 @@ namespace CreativeCoders.HomeMatic.Core.Parameters; /// [PublicAPI] [Flags] -public enum ParameterFlags +public enum ParameterUiAttributes { /// /// No flags are set. diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs b/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs index c6429e4..866f6fa 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs @@ -105,8 +105,8 @@ public class DeviceDescription /// Gets or sets the reception mode flags for this device. Only present for BidCoS-RF devices. /// /// A bitwise combination of values. - [XmlRpcStructMember("RX_MODE", DefaultValue = RxMode.None, Converter = typeof(FlagsMemberValueConverter))] - public RxMode RxMode { get; set; } + [XmlRpcStructMember("RX_MODE", DefaultValue = RxModes.None, Converter = typeof(FlagsMemberValueConverter))] + public RxModes RxMode { get; set; } /// /// Gets or sets the address of the paired channel in a button group. Only present for grouped channels. @@ -149,7 +149,8 @@ public class DeviceDescription /// Gets or sets the current firmware update state of the device. Only present for devices. /// /// One of the values indicating the update progress. - [XmlRpcStructMember("FIRMWARE_UPDATE_STATE", DefaultValue = DeviceFirmwareUpdateState.None, Converter = typeof(DeviceFirmwareUpdateStateValueConverter))] + [XmlRpcStructMember("FIRMWARE_UPDATE_STATE", DefaultValue = DeviceFirmwareUpdateState.None, + Converter = typeof(DeviceFirmwareUpdateStateValueConverter))] public DeviceFirmwareUpdateState FirmwareUpdateState { get; set; } /// @@ -166,7 +167,8 @@ public class DeviceDescription /// Gets or sets the direction of this channel in a direct device link. Only present for channels. /// /// One of the values. - [XmlRpcStructMember("DIRECTION", DefaultValue = ChannelDirection.None, Converter = typeof(EnumMemberValueConverter))] + [XmlRpcStructMember("DIRECTION", DefaultValue = ChannelDirection.None, + Converter = typeof(EnumMemberValueConverter))] public ChannelDirection ChannelDirection { get; set; } /// @@ -199,4 +201,4 @@ public class DeviceDescription /// if this entry describes a channel; otherwise, . /// public bool IsChannel => !IsDevice; -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxModes.cs similarity index 97% rename from source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs rename to source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxModes.cs index d4e3182..c6d2d06 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxMode.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxModes.cs @@ -11,7 +11,7 @@ namespace CreativeCoders.HomeMatic.XmlRpc.Parameters; /// [PublicAPI] [Flags] -public enum RxMode +public enum RxModes { /// /// No reception mode is set. diff --git a/source/CreativeCoders.HomeMatic/CcuDevice.cs b/source/CreativeCoders.HomeMatic/CcuDevice.cs index e690de7..9c75f70 100644 --- a/source/CreativeCoders.HomeMatic/CcuDevice.cs +++ b/source/CreativeCoders.HomeMatic/CcuDevice.cs @@ -9,7 +9,7 @@ public class CcuDevice(IHomeMaticXmlRpcApi api) : CcuDeviceBase(api), ICcuDevice { public string Name { get; set; } = string.Empty; - public required RxMode RxMode { get; init; } + public required RxModes RxMode { get; init; } public required int RfAddress { get; init; } diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs index cf1d495..44896fd 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; using CreativeCoders.HomeMatic.Core; @@ -11,7 +12,9 @@ namespace CreativeCoders.HomeMatic; public class CcuDeviceBuilder : ObjectBuilderBase { - #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + [SuppressMessage("csharpsquid", "S3459", + Justification = "Fields are set via WithField method and not directly assigned to.")] private CcuDeviceUri? _uri; private DeviceDescription? _deviceDescription; @@ -51,7 +54,7 @@ public override CcuDevice Build() Version = _deviceDescription?.Version ?? 0, IsAesActive = _deviceDescription?.IsAesActive ?? false, Interface = _deviceDescription?.Interface ?? string.Empty, - RxMode = _deviceDescription?.RxMode ?? RxMode.None, + RxMode = _deviceDescription?.RxMode ?? RxModes.None, RfAddress = _deviceDescription?.RfAddress ?? 0, Firmware = _deviceDescription?.Firmware ?? string.Empty, AvailableFirmware = _deviceDescription?.AvailableFirmware ?? string.Empty, @@ -110,6 +113,7 @@ public CcuDeviceBuilder WithAllDevices(IEnumerable devices) public abstract class ObjectBuilderBase where TBuilderImpl : class, new() { + [SuppressMessage("csharpsquid", "S3011", Justification = "Reflection only used for writing own private fields")] protected TBuilderImpl WithField(Expression> property, TProperty value) { var member = (MemberExpression)property.Body; diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs index 0578b8f..ac6dfc0 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs @@ -22,7 +22,7 @@ public void Build_WithValidUriAndApi_ReturnsCcuDeviceWithCorrectProperties() Index = 2, IsAesActive = true, Interface = "TestInterface", - RxMode = RxMode.Always, + RxMode = RxModes.Always, Group = "TestGroup", RfAddress = 12345, Firmware = "1.0.0", @@ -57,7 +57,7 @@ public void Build_WithValidUriAndApi_ReturnsCcuDeviceWithCorrectProperties() ccuDevice.Version.Should().Be(1); ccuDevice.IsAesActive.Should().BeTrue(); ccuDevice.Interface.Should().Be("TestInterface"); - ccuDevice.RxMode.Should().Be(RxMode.Always); + ccuDevice.RxMode.Should().Be(RxModes.Always); ccuDevice.RfAddress.Should().Be(12345); ccuDevice.Firmware.Should().Be("1.0.0"); ccuDevice.AvailableFirmware.Should().Be("1.1.0"); @@ -133,7 +133,7 @@ public void Build_WithNullDeviceDescription_ReturnsCcuDeviceWithDefaultPropertie ccuDevice.Version.Should().Be(0); ccuDevice.IsAesActive.Should().BeFalse(); ccuDevice.Interface.Should().Be(string.Empty); - ccuDevice.RxMode.Should().Be(RxMode.None); + ccuDevice.RxMode.Should().Be(RxModes.None); ccuDevice.RfAddress.Should().Be(0); ccuDevice.Firmware.Should().Be(string.Empty); ccuDevice.AvailableFirmware.Should().Be(string.Empty); From e34bae0de4c675a561f7a5d1c1159ae9ab7533ac Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:59:52 +0200 Subject: [PATCH 07/15] feat(homeMatic): add `CcuRoutingTable` for efficient device-to-client mapping - Introduced `CcuRoutingTable` as a thread-safe implementation of `ICcuRoutingTable` using `ConcurrentDictionary`. - Updated `MultiCcuClient` to leverage routing table for optimized client resolution and per-device operations. - Added tests for `CcuRoutingTable` and `MultiCcuClient` to validate new routing logic and caching behavior. --- .../ICcuRoutingTable.cs | 45 +++++ .../CcuRoutingTable.cs | 56 ++++++ .../MultiCcuClient.cs | 114 +++++++++-- .../MultiCcuClientFactory.cs | 2 +- .../CcuRoutingTableTests.cs | 90 +++++++++ .../MultiCcuClientTests.cs | 179 ++++++++++++++++++ 6 files changed, 467 insertions(+), 19 deletions(-) create mode 100644 source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs create mode 100644 source/CreativeCoders.HomeMatic/CcuRoutingTable.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/CcuRoutingTableTests.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs diff --git a/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs b/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs new file mode 100644 index 0000000..ab2a027 --- /dev/null +++ b/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace CreativeCoders.HomeMatic.Core; + +/// +/// Maintains a mapping between HomeMatic device addresses and the that owns them. +/// +/// +/// The routing table enables implementations to avoid querying every configured +/// CCU for each per-device call. Implementations must be safe for concurrent use. +/// +public interface ICcuRoutingTable +{ + /// + /// Attempts to resolve the that owns the device with the given address. + /// + /// The device address (without channel suffix). + /// When this method returns, contains the mapped client if found; otherwise . + /// if a mapping was found; otherwise, . + bool TryGetClient(string address, out ICcuClient? client); + + /// + /// Registers a mapping from the given device address to the specified . + /// + /// The device address (without channel suffix). + /// The owning CCU client. + void Register(string address, ICcuClient client); + + /// + /// Registers multiple address-to-client mappings in one call. + /// + /// The mappings to register. + void Register(IEnumerable> entries); + + /// + /// Removes the mapping for the given device address if present. + /// + /// The device address (without channel suffix). + void Invalidate(string address); + + /// + /// Removes all mappings from the routing table. + /// + void Clear(); +} diff --git a/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs b/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs new file mode 100644 index 0000000..8be85b5 --- /dev/null +++ b/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs @@ -0,0 +1,56 @@ +using System.Collections.Concurrent; +using CreativeCoders.Core; +using CreativeCoders.HomeMatic.Core; + +namespace CreativeCoders.HomeMatic; + +/// +/// Default thread-safe implementation of backed by a +/// . +/// +public class CcuRoutingTable : ICcuRoutingTable +{ + private readonly ConcurrentDictionary _routes = new(); + + /// + public bool TryGetClient(string address, out ICcuClient? client) + { + Ensure.IsNotNullOrWhitespace(address); + + return _routes.TryGetValue(address, out client); + } + + /// + public void Register(string address, ICcuClient client) + { + Ensure.IsNotNullOrWhitespace(address); + Ensure.NotNull(client); + + _routes[address] = client; + } + + /// + public void Register(IEnumerable> entries) + { + Ensure.NotNull(entries); + + foreach (var entry in entries) + { + Register(entry.Key, entry.Value); + } + } + + /// + public void Invalidate(string address) + { + Ensure.IsNotNullOrWhitespace(address); + + _routes.TryRemove(address, out _); + } + + /// + public void Clear() + { + _routes.Clear(); + } +} diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs index d7334c8..c8a4f35 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs @@ -1,36 +1,97 @@ +using CreativeCoders.Core; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Core.Devices; namespace CreativeCoders.HomeMatic; -public class MultiCcuClient( - IEnumerable ccuClients) : IMultiCcuClient +public class MultiCcuClient : IMultiCcuClient { - public Task> GetDevicesAsync() + private readonly IReadOnlyList _ccuClients; + + private readonly ICcuRoutingTable _routingTable; + + public MultiCcuClient(IEnumerable ccuClients) + : this(ccuClients, new CcuRoutingTable()) + { + } + + public MultiCcuClient(IEnumerable ccuClients, ICcuRoutingTable routingTable) + { + Ensure.NotNull(ccuClients); + + _ccuClients = ccuClients.ToList(); + _routingTable = Ensure.NotNull(routingTable); + } + + public async Task> GetDevicesAsync() + { + var results = await GetDataFromClientsAsync(x => x.GetDevicesAsync()).ConfigureAwait(false); + + // Populate the routing table so that subsequent per-device calls can skip the full scan. + RegisterRoutes(results.SelectMany(pair => pair.Items.Select(item => (item.Uri.Address, pair.Client)))); + + return results.SelectMany(pair => pair.Items); + } + + public Task GetDeviceAsync(string address) { - return GetDataFromClientsAsync(x => x.GetDevicesAsync()); + Ensure.IsNotNullOrWhitespace(address); + + return InvokeWithRoutingAsync(address, (client, deviceAddress) => client.GetDeviceAsync(deviceAddress)); } - public async Task GetDeviceAsync(string address) + public async Task> GetCompleteDevicesAsync() { - return (await GetDevicesAsync().ConfigureAwait(false)).FirstOrDefault(x => x.Uri.Address == address) ?? - throw new KeyNotFoundException($"Device with address '{address}' not found."); + var results = await GetDataFromClientsAsync(x => x.GetCompleteDevicesAsync()).ConfigureAwait(false); + + RegisterRoutes(results.SelectMany(pair => + pair.Items.Select(item => (item.DeviceData.Uri.Address, pair.Client)))); + + return results.SelectMany(pair => pair.Items); } - public Task> GetCompleteDevicesAsync() + public Task GetCompleteDeviceAsync(string address) { - return GetDataFromClientsAsync(x => x.GetCompleteDevicesAsync()); + Ensure.IsNotNullOrWhitespace(address); + + return InvokeWithRoutingAsync(address, + (client, deviceAddress) => client.GetCompleteDeviceAsync(deviceAddress)); } - public async Task GetCompleteDeviceAsync(string address) + // Generic helper that routes a per-device call through the routing table. Can be reused by future + // per-device methods without duplicating the lookup/fallback logic. + private async Task InvokeWithRoutingAsync(string address, + Func> func) { - foreach (var ccuClient in ccuClients) + var deviceAddress = NormalizeAddress(address); + + if (_routingTable.TryGetClient(deviceAddress, out var cachedClient) && cachedClient is not null) { try { - var completeDevice = await ccuClient.GetCompleteDeviceAsync(address).ConfigureAwait(false); + return await func(cachedClient, address).ConfigureAwait(false); + } + catch (KeyNotFoundException) + { + // Cached mapping is stale; drop it and fall back to probing the remaining clients. + _routingTable.Invalidate(deviceAddress); + } + } - return completeDevice; + foreach (var ccuClient in _ccuClients) + { + if (ReferenceEquals(ccuClient, cachedClient)) + { + continue; + } + + try + { + var result = await func(ccuClient, address).ConfigureAwait(false); + + _routingTable.Register(deviceAddress, ccuClient); + + return result; } catch (KeyNotFoundException) { @@ -40,17 +101,34 @@ public async Task GetCompleteDeviceAsync(string address) throw new KeyNotFoundException($"Device with address '{address}' not found."); } - private async Task> GetDataFromClientsAsync(Func>> func) + private void RegisterRoutes(IEnumerable<(string Address, ICcuClient Client)> entries) + { + _routingTable.Register(entries + .Where(entry => !string.IsNullOrWhiteSpace(entry.Address)) + .Select(entry => new KeyValuePair(NormalizeAddress(entry.Address), entry.Client))); + } + + // Device addresses may be suffixed with a channel index (e.g. "ABC0001234:1"). Routing is performed + // on the device level, so we strip the channel part for lookups and registrations. + private static string NormalizeAddress(string address) + { + var separatorIndex = address.IndexOf(':'); + + return separatorIndex < 0 ? address : address[..separatorIndex]; + } + + private async Task Items)>> GetDataFromClientsAsync( + Func>> func) { - var dataFromClients = new List>(); + var dataFromClients = new List<(ICcuClient Client, IEnumerable Items)>(); - foreach (var ccuClient in ccuClients) + foreach (var ccuClient in _ccuClients) { var data = await func(ccuClient).ConfigureAwait(false); - dataFromClients.Add(data); + dataFromClients.Add((ccuClient, data)); } - return dataFromClients.SelectMany(x => x); + return dataFromClients; } } diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs index 8464db7..430155e 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs @@ -19,6 +19,6 @@ public IMultiCcuClientFactory AddCcu(string ccuName, string host, string userNam public IMultiCcuClient Build() { - return new MultiCcuClient(_ccuClients); + return new MultiCcuClient(_ccuClients, new CcuRoutingTable()); } } diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuRoutingTableTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuRoutingTableTests.cs new file mode 100644 index 0000000..9bf0e87 --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuRoutingTableTests.cs @@ -0,0 +1,90 @@ +using CreativeCoders.HomeMatic.Core; +using FakeItEasy; +using AwesomeAssertions; + +namespace CreativeCoders.HomeMatic.Tests; + +public class CcuRoutingTableTests +{ + [Fact] + public void TryGetClient_UnknownAddress_ReturnsFalse() + { + var table = new CcuRoutingTable(); + + var found = table.TryGetClient("ABC0001234", out var client); + + found.Should().BeFalse(); + client.Should().BeNull(); + } + + [Fact] + public void Register_ThenTryGetClient_ReturnsRegisteredClient() + { + var table = new CcuRoutingTable(); + var ccuClient = A.Fake(); + + table.Register("ABC0001234", ccuClient); + + table.TryGetClient("ABC0001234", out var resolved).Should().BeTrue(); + resolved.Should().BeSameAs(ccuClient); + } + + [Fact] + public void Register_SameAddressTwice_OverwritesExistingEntry() + { + var table = new CcuRoutingTable(); + var firstClient = A.Fake(); + var secondClient = A.Fake(); + + table.Register("ABC0001234", firstClient); + table.Register("ABC0001234", secondClient); + + table.TryGetClient("ABC0001234", out var resolved).Should().BeTrue(); + resolved.Should().BeSameAs(secondClient); + } + + [Fact] + public void RegisterBulk_AddsAllEntries() + { + var table = new CcuRoutingTable(); + var clientA = A.Fake(); + var clientB = A.Fake(); + + table.Register( + [ + new KeyValuePair("A", clientA), + new KeyValuePair("B", clientB) + ]); + + table.TryGetClient("A", out var resolvedA).Should().BeTrue(); + resolvedA.Should().BeSameAs(clientA); + + table.TryGetClient("B", out var resolvedB).Should().BeTrue(); + resolvedB.Should().BeSameAs(clientB); + } + + [Fact] + public void Invalidate_RemovesEntry() + { + var table = new CcuRoutingTable(); + var ccuClient = A.Fake(); + table.Register("ABC0001234", ccuClient); + + table.Invalidate("ABC0001234"); + + table.TryGetClient("ABC0001234", out _).Should().BeFalse(); + } + + [Fact] + public void Clear_RemovesAllEntries() + { + var table = new CcuRoutingTable(); + table.Register("A", A.Fake()); + table.Register("B", A.Fake()); + + table.Clear(); + + table.TryGetClient("A", out _).Should().BeFalse(); + table.TryGetClient("B", out _).Should().BeFalse(); + } +} diff --git a/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs b/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs new file mode 100644 index 0000000..3f192dd --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs @@ -0,0 +1,179 @@ +using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.Core.Devices; +using CreativeCoders.HomeMatic.XmlRpc; +using FakeItEasy; +using AwesomeAssertions; + +namespace CreativeCoders.HomeMatic.Tests; + +public class MultiCcuClientTests +{ + private const string DeviceA = "ABC0001234"; + private const string DeviceB = "ABC0005678"; + + [Fact] + public async Task GetDeviceAsync_UnknownDevice_ProbesAllClientsAndThrows() + { + var clientA = CreateClientWithDevices(); + var clientB = CreateClientWithDevices(); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + var act = () => multi.GetDeviceAsync("UNKNOWN"); + + await act.Should().ThrowAsync(); + A.CallTo(() => clientA.GetDeviceAsync("UNKNOWN")).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetDeviceAsync("UNKNOWN")).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task GetDeviceAsync_SecondCall_UsesOnlyOwningClient() + { + var clientA = CreateClientWithDevices(DeviceA); + var clientB = CreateClientWithDevices(DeviceB); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + await multi.GetDeviceAsync(DeviceB); + await multi.GetDeviceAsync(DeviceB); + + // First call probes both until success, second call hits only clientB thanks to routing table. + A.CallTo(() => clientA.GetDeviceAsync(DeviceB)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetDeviceAsync(DeviceB)).MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task GetDevicesAsync_PopulatesRoutingTableSoSubsequentGetDeviceSkipsOtherClients() + { + var clientA = CreateClientWithDevices(DeviceA); + var clientB = CreateClientWithDevices(DeviceB); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + _ = await multi.GetDevicesAsync(); + + await multi.GetDeviceAsync(DeviceA); + await multi.GetDeviceAsync(DeviceB); + + A.CallTo(() => clientA.GetDeviceAsync(DeviceA)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientA.GetDeviceAsync(DeviceB)).MustNotHaveHappened(); + A.CallTo(() => clientB.GetDeviceAsync(DeviceB)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetDeviceAsync(DeviceA)).MustNotHaveHappened(); + } + + [Fact] + public async Task GetDeviceAsync_ChannelAddress_UsesDeviceLevelRoute() + { + var clientA = CreateClientWithDevices(DeviceA); + var clientB = CreateClientWithDevices(DeviceB); + + var channelDevice = A.Fake(); + A.CallTo(() => channelDevice.Uri).Returns(new CcuDeviceUri + { + CcuHost = "ccu-b", Kind = CcuDeviceKind.HomeMatic, Address = $"{DeviceB}:1" + }); + A.CallTo(() => clientB.GetDeviceAsync($"{DeviceB}:1")).Returns(Task.FromResult(channelDevice)); + A.CallTo(() => clientA.GetDeviceAsync($"{DeviceB}:1")).ThrowsAsync(new KeyNotFoundException()); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + // Populate routing for DeviceB via bulk call. + _ = await multi.GetDevicesAsync(); + + // Request a channel address "DeviceB:1"; routing should strip the channel suffix and go to clientB. + await multi.GetDeviceAsync($"{DeviceB}:1"); + + A.CallTo(() => clientA.GetDeviceAsync($"{DeviceB}:1")).MustNotHaveHappened(); + A.CallTo(() => clientB.GetDeviceAsync($"{DeviceB}:1")).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task GetDeviceAsync_CachedClientFails_FallsBackToOtherClientsAndUpdatesRoute() + { + var clientA = CreateClientWithDevices(DeviceA); + var clientB = CreateClientWithDevices(DeviceA); + + var routingTable = new CcuRoutingTable(); + // Pre-seed stale mapping: DeviceA owned by clientA even though it actually lives on clientB as well. + // Make clientA fail to simulate a stale entry. + A.CallTo(() => clientA.GetDeviceAsync(DeviceA)).ThrowsAsync(new KeyNotFoundException()); + routingTable.Register(DeviceA, clientA); + + var multi = new MultiCcuClient([clientA, clientB], routingTable); + + var device = await multi.GetDeviceAsync(DeviceA); + + device.Should().NotBeNull(); + A.CallTo(() => clientA.GetDeviceAsync(DeviceA)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetDeviceAsync(DeviceA)).MustHaveHappenedOnceExactly(); + + routingTable.TryGetClient(DeviceA, out var resolved).Should().BeTrue(); + resolved.Should().BeSameAs(clientB); + } + + [Fact] + public async Task GetCompleteDeviceAsync_SecondCall_UsesOnlyOwningClient() + { + var clientA = CreateClientWithCompleteDevices(); + var clientB = CreateClientWithCompleteDevices(DeviceA); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + await multi.GetCompleteDeviceAsync(DeviceA); + await multi.GetCompleteDeviceAsync(DeviceA); + + A.CallTo(() => clientA.GetCompleteDeviceAsync(DeviceA)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetCompleteDeviceAsync(DeviceA)).MustHaveHappenedTwiceExactly(); + } + + private static ICcuClient CreateClientWithDevices(params string[] addresses) + { + var client = A.Fake(); + var devices = addresses.Select(address => + { + var device = A.Fake(); + A.CallTo(() => device.Uri).Returns(new CcuDeviceUri + { + CcuHost = "ccu", Kind = CcuDeviceKind.HomeMatic, Address = address + }); + A.CallTo(() => client.GetDeviceAsync(address)).Returns(Task.FromResult(device)); + return device; + }).ToArray(); + + A.CallTo(() => client.GetDevicesAsync()).Returns(Task.FromResult(devices.AsEnumerable())); + + // Default: unknown addresses throw KeyNotFoundException. + A.CallTo(() => client.GetDeviceAsync(A.That.Matches(x => !addresses.Contains(x)))) + .ThrowsAsync(new KeyNotFoundException()); + + return client; + } + + private static ICcuClient CreateClientWithCompleteDevices(params string[] addresses) + { + var client = A.Fake(); + + var devices = addresses.Select(address => + { + var deviceData = A.Fake(); + A.CallTo(() => deviceData.Uri).Returns(new CcuDeviceUri + { + CcuHost = "ccu", Kind = CcuDeviceKind.HomeMatic, Address = address + }); + + var device = A.Fake(); + A.CallTo(() => device.DeviceData).Returns(deviceData); + + A.CallTo(() => client.GetCompleteDeviceAsync(address)).Returns(Task.FromResult(device)); + + return device; + }).ToArray(); + + A.CallTo(() => client.GetCompleteDevicesAsync()).Returns(Task.FromResult(devices.AsEnumerable())); + + A.CallTo(() => client.GetCompleteDeviceAsync(A.That.Matches(x => !addresses.Contains(x)))) + .ThrowsAsync(new KeyNotFoundException()); + + return client; + } +} From 14a95785df4e9f66a99912138149946cc844e82f Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:44:34 +0200 Subject: [PATCH 08/15] feat(tests): add comprehensive unit tests for HomeMatic components - Added new test classes: `CcuDeviceBaseTests`, `CcuDeviceTests`, `CompleteCcuDeviceBuilderTests`, `MultiCcuClientFactoryTests`, and `XmlRpcApiConnectionTests`. - Increased code coverage for device parameter mapping, factory chaining, and error handling. - Refactored existing tests to ensure consistency with updated namespaces and conventions. --- .../MultiCcuClient.cs | 29 +- .../CcuClientTests.cs | 270 ++++++++++++++++++ .../CcuDeviceBaseTests.cs | 148 ++++++++++ .../CcuDeviceBuilderTests.cs | 158 +++++++++- .../CcuDeviceTests.cs | 107 +++++++ .../CompleteCcuDeviceBuilderTests.cs | 215 ++++++++++++++ .../Exporting/DeviceExporterTests.cs | 1 - .../MultiCcuClientFactoryTests.cs | 79 +++++ .../MultiCcuClientTests.cs | 190 ++++++++++++ .../XmlRpcApiConnectionTests.cs | 66 +++++ 10 files changed, 1239 insertions(+), 24 deletions(-) create mode 100644 tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBaseTests.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/CcuDeviceTests.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/CompleteCcuDeviceBuilderTests.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientFactoryTests.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/XmlRpcApiConnectionTests.cs diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs index c8a4f35..47e6cc0 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs @@ -1,27 +1,16 @@ +using System.Diagnostics.CodeAnalysis; using CreativeCoders.Core; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Core.Devices; namespace CreativeCoders.HomeMatic; -public class MultiCcuClient : IMultiCcuClient +public class MultiCcuClient(IEnumerable ccuClients, ICcuRoutingTable routingTable) + : IMultiCcuClient { - private readonly IReadOnlyList _ccuClients; + private readonly IReadOnlyList _ccuClients = Ensure.NotNull(ccuClients).ToList(); - private readonly ICcuRoutingTable _routingTable; - - public MultiCcuClient(IEnumerable ccuClients) - : this(ccuClients, new CcuRoutingTable()) - { - } - - public MultiCcuClient(IEnumerable ccuClients, ICcuRoutingTable routingTable) - { - Ensure.NotNull(ccuClients); - - _ccuClients = ccuClients.ToList(); - _routingTable = Ensure.NotNull(routingTable); - } + private readonly ICcuRoutingTable _routingTable = Ensure.NotNull(routingTable); public async Task> GetDevicesAsync() { @@ -78,13 +67,8 @@ private async Task InvokeWithRoutingAsync(string address, } } - foreach (var ccuClient in _ccuClients) + foreach (var ccuClient in _ccuClients.Where(ccuClient => !ReferenceEquals(ccuClient, cachedClient))) { - if (ReferenceEquals(ccuClient, cachedClient)) - { - continue; - } - try { var result = await func(ccuClient, address).ConfigureAwait(false); @@ -95,6 +79,7 @@ private async Task InvokeWithRoutingAsync(string address, } catch (KeyNotFoundException) { + // Device not found on this client; try the next one. } } diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs index 67f0c39..0ed930f 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs @@ -256,6 +256,276 @@ public async Task GetDevicesAsync_EmptyDeviceDetails_ReturnsDevicesWithEmptyName .Match(x => x.Name == string.Empty); } + [Fact] + public async Task GetDeviceAsync_UnknownAddress_ThrowsKeyNotFoundException() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi); + + // Act + var act = () => ccuClient.GetDeviceAsync("UNKNOWN"); + + // Assert + await act.Should().ThrowAsync() + .WithMessage("Device with address 'UNKNOWN' not found."); + } + + [Fact] + public async Task GetDeviceAsync_MatchingAddressCaseInsensitive_ReturnsDevice() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult>( + [ + new DeviceDescription { Address = "ABC1234", Interface = "BidCos-RF" } + ])); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi); + + // Act + var device = await ccuClient.GetDeviceAsync("abc1234"); + + // Assert + device.Uri.Address.Should().Be("ABC1234"); + } + + [Fact] + public async Task GetCompleteDevicesAsync_DelegatesBuildingToCompleteCcuDeviceBuilder() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + var completeBuilder = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult>( + [ + new DeviceDescription { Address = "A", Interface = "BidCos-RF" }, + new DeviceDescription { Address = "B", Interface = "BidCos-RF" } + ])); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + var completeDevice = A.Fake(); + A.CallTo(() => completeBuilder.BuildAsync(A._)) + .Returns(Task.FromResult(completeDevice)); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi, completeBuilder); + + // Act + var devices = (await ccuClient.GetCompleteDevicesAsync()).ToList(); + + // Assert + devices.Should().HaveCount(2); + A.CallTo(() => completeBuilder.BuildAsync(A._)) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task GetCompleteDevicesAsync_NoDevices_ReturnsEmptyAndDoesNotCallBuilder() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + var completeBuilder = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi, completeBuilder); + + // Act + var devices = await ccuClient.GetCompleteDevicesAsync(); + + // Assert + devices.Should().BeEmpty(); + A.CallTo(() => completeBuilder.BuildAsync(A._)).MustNotHaveHappened(); + } + + [Fact] + public async Task GetCompleteDevicesAsync_ReturnsBuilderProducedDevices() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + var completeBuilder = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult>( + [ + new DeviceDescription { Address = "A", Interface = "BidCos-RF" }, + new DeviceDescription { Address = "B", Interface = "BidCos-RF" } + ])); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + var completeDeviceA = A.Fake(); + var completeDeviceB = A.Fake(); + + A.CallTo(() => completeBuilder.BuildAsync(A.That.Matches(d => d.Uri.Address == "A"))) + .Returns(Task.FromResult(completeDeviceA)); + A.CallTo(() => completeBuilder.BuildAsync(A.That.Matches(d => d.Uri.Address == "B"))) + .Returns(Task.FromResult(completeDeviceB)); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi, completeBuilder); + + // Act + var devices = (await ccuClient.GetCompleteDevicesAsync()).ToList(); + + // Assert + devices.Should().HaveCount(2); + devices.Should().ContainInOrder(completeDeviceA, completeDeviceB); + } + + [Fact] + public async Task GetCompleteDeviceAsync_KnownAddress_ReturnsBuilderResult() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + var completeBuilder = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult>( + [ + new DeviceDescription { Address = "ABC1234", Interface = "BidCos-RF" } + ])); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + var expected = A.Fake(); + A.CallTo(() => completeBuilder.BuildAsync(A.That.Matches(d => d.Uri.Address == "ABC1234"))) + .Returns(Task.FromResult(expected)); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi, completeBuilder); + + // Act + var device = await ccuClient.GetCompleteDeviceAsync("abc1234"); + + // Assert + device.Should().BeSameAs(expected); + A.CallTo(() => completeBuilder.BuildAsync(A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task GetCompleteDeviceAsync_UnknownAddress_ThrowsKeyNotFoundException() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi); + + // Act + var act = () => ccuClient.GetCompleteDeviceAsync("UNKNOWN"); + + // Assert + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task GetCompleteDevicesAsync_BuilderThrows_PropagatesException() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + var completeBuilder = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult>( + [ + new DeviceDescription { Address = "A", Interface = "BidCos-RF" } + ])); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + A.CallTo(() => completeBuilder.BuildAsync(A._)) + .ThrowsAsync(new InvalidOperationException("boom")); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi, completeBuilder); + + // Act + var act = () => ccuClient.GetCompleteDevicesAsync(); + + // Assert + await act.Should().ThrowAsync().WithMessage("boom"); + } + + [Fact] + public async Task GetCompleteDeviceAsync_BuilderThrows_PropagatesException() + { + // Arrange + var jsonRpcClient = A.Fake(); + var homeMaticXmlRpcApi = A.Fake(); + var homeMaticIpXmlRpcApi = A.Fake(); + var completeBuilder = A.Fake(); + + A.CallTo(() => jsonRpcClient.ListAllDetailsAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + A.CallTo(() => homeMaticXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult>( + [ + new DeviceDescription { Address = "ABC1234", Interface = "BidCos-RF" } + ])); + A.CallTo(() => homeMaticIpXmlRpcApi.ListDevicesAsync()) + .Returns(Task.FromResult(Array.Empty().AsEnumerable())); + + A.CallTo(() => completeBuilder.BuildAsync(A._)) + .ThrowsAsync(new InvalidOperationException("boom")); + + var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi, completeBuilder); + + // Act + var act = () => ccuClient.GetCompleteDeviceAsync("ABC1234"); + + // Assert + await act.Should().ThrowAsync().WithMessage("boom"); + } + private static CcuClient CreateCcuClient(IHomeMaticJsonRpcClient jsonRpcClient, IHomeMaticXmlRpcApi homeMaticXmlRpcApi, IHomeMaticXmlRpcApi homeMaticIpXmlRpcApi, diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBaseTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBaseTests.cs new file mode 100644 index 0000000..f492ef9 --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBaseTests.cs @@ -0,0 +1,148 @@ +using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.XmlRpc; +using CreativeCoders.HomeMatic.XmlRpc.Client; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; +using FakeItEasy; +using AwesomeAssertions; + +namespace CreativeCoders.HomeMatic.Tests; + +public class CcuDeviceBaseTests +{ + private const string DeviceAddress = "BIDCOS:1"; + + [Fact] + public async Task GetParamSetValuesAsync_ReturnsMappedParamSetValuesFromApi() + { + // Arrange + var api = A.Fake(); + var paramSet = new Dictionary + { + ["TEMPERATURE"] = 21.5, + ["HUMIDITY"] = 45 + }; + + A.CallTo(() => api.GetParamSetAsync(DeviceAddress, "VALUES")) + .Returns(Task.FromResult(paramSet)); + + var device = CreateDevice(api); + + // Act + var values = (await device.GetParamSetValuesAsync("VALUES")).ToList(); + + // Assert + values.Should().HaveCount(2); + values.Should().Contain(v => v.Name == "TEMPERATURE" && Math.Abs((double)v.Value - 21.5) < 0.01); + values.Should().Contain(v => v.Name == "HUMIDITY" && (int)v.Value == 45); + } + + [Fact] + public async Task GetParamSetValuesAsync_EmptyApiResult_ReturnsEmptyEnumerable() + { + // Arrange + var api = A.Fake(); + A.CallTo(() => api.GetParamSetAsync(DeviceAddress, "MASTER")) + .Returns(Task.FromResult(new Dictionary())); + + var device = CreateDevice(api); + + // Act + var values = await device.GetParamSetValuesAsync("MASTER"); + + // Assert + values.Should().BeEmpty(); + } + + [Fact] + public async Task GetParamSetDescriptionsAsync_ReturnsMappedDescriptionsWithParamSetKey() + { + // Arrange + var api = A.Fake(); + var descriptions = new Dictionary + { + ["TEMPERATURE"] = new() + { + Id = "TEMPERATURE", + DefaultValue = 20.0, + MinValue = 4.5, + MaxValue = 30.5, + Type = "FLOAT", + DataType = ParameterDataType.Float, + Unit = "°C", + TabOrder = 1, + Control = "THERMOSTAT.SET_TEMPERATURE", + ValuesList = [], + SpecialValues = [] + } + }; + + A.CallTo(() => api.GetParameterDescriptionAsync(DeviceAddress, "VALUES")) + .Returns(Task.FromResult(descriptions)); + + var device = CreateDevice(api); + + // Act + var result = await device.GetParamSetDescriptionsAsync("VALUES"); + + // Assert + result.ParamSetKey.Should().Be("VALUES"); + var items = result.Items.ToList(); + items.Should().HaveCount(1); + + var item = items[0]; + item.Id.Should().Be("TEMPERATURE"); + item.DefaultValue.Should().Be(20.0); + item.MinValue.Should().Be(4.5); + item.MaxValue.Should().Be(30.5); + item.Type.Should().Be("FLOAT"); + item.DataType.Should().Be(ParameterDataType.Float); + item.Unit.Should().Be("°C"); + item.TabOrder.Should().Be(1); + item.Control.Should().Be("THERMOSTAT.SET_TEMPERATURE"); + } + + [Fact] + public async Task GetParamSetDescriptionsAsync_EmptyApiResult_ReturnsResultWithNoItems() + { + // Arrange + var api = A.Fake(); + A.CallTo(() => api.GetParameterDescriptionAsync(DeviceAddress, "MASTER")) + .Returns(Task.FromResult(new Dictionary())); + + var device = CreateDevice(api); + + // Act + var result = await device.GetParamSetDescriptionsAsync("MASTER"); + + // Assert + result.ParamSetKey.Should().Be("MASTER"); + result.Items.Should().BeEmpty(); + } + + private static CcuDevice CreateDevice(IHomeMaticXmlRpcApi api) + { + return new CcuDevice(api) + { + Uri = new CcuDeviceUri + { + CcuHost = "localhost", + Kind = CcuDeviceKind.HomeMatic, + Address = DeviceAddress + }, + DeviceType = "TestType", + IsAesActive = false, + Interface = "BidCos-RF", + Version = 1, + Roaming = false, + ParamSets = ["MASTER", "VALUES"], + RxMode = RxModes.Always, + RfAddress = 0, + Firmware = "1.0.0", + AvailableFirmware = "1.0.0", + CanBeUpdated = false, + FirmwareUpdateState = DeviceFirmwareUpdateState.None, + Channels = [] + }; + } +} diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs index ac6dfc0..110b537 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs @@ -1,5 +1,4 @@ using CreativeCoders.HomeMatic.Core; -using CreativeCoders.HomeMatic.Core.Devices; using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Client; using CreativeCoders.HomeMatic.XmlRpc.Devices; @@ -108,6 +107,114 @@ public void Build_WithoutApi_ThrowsInvalidOperationException() .WithMessage("Uri, Api and Devices must be set"); } + [Fact] + public void Build_WithoutDevices_ThrowsInvalidOperationException() + { + // Arrange + var api = A.Fake(); + var uri = new CcuDeviceUri + { + CcuHost = "localhost", + Kind = CcuDeviceKind.HomeMatic, + Address = "1234567890" + }; + + var builder = new CcuDeviceBuilder() + .WithUri(uri) + .WithApi(api); + + // Act + Action act = () => builder.Build(); + + // Assert + act.Should().Throw() + .WithMessage("Uri, Api and Devices must be set"); + } + + [Fact] + public void Build_WithDeviceDescriptionAndChildDevices_CreatesChannelsOrderedByIndex() + { + // Arrange - parent device has two children (out of order) and an unrelated device. + const string parentAddress = "PARENT1"; + + var parent = new DeviceDescription + { + Address = parentAddress, + Parent = string.Empty, + DeviceType = "ParentType", + ParamSets = ["MASTER"] + }; + + var channel2 = new DeviceDescription + { + Address = parentAddress + ":2", + Parent = parentAddress, + DeviceType = "ChannelType", + Index = 2, + Group = "G2", + ChannelDirection = ChannelDirection.Sender, + Interface = "BidCos-RF", + Version = 7, + IsAesActive = true, + Roaming = false, + ParamSets = ["VALUES"] + }; + + var channel1 = new DeviceDescription + { + Address = parentAddress + ":1", + Parent = parentAddress, + DeviceType = "ChannelType", + Index = 1, + Group = "G1", + ChannelDirection = ChannelDirection.Receiver, + Interface = "BidCos-RF", + Version = 7, + IsAesActive = false, + Roaming = false, + ParamSets = ["VALUES"] + }; + + var unrelated = new DeviceDescription + { + Address = "OTHER:1", + Parent = "OTHER", + DeviceType = "ChannelType", + Index = 1 + }; + + var api = A.Fake(); + var uri = new CcuDeviceUri + { + CcuHost = "localhost", + CcuName = "ccu", + Kind = CcuDeviceKind.HomeMatic, + Address = parentAddress + }; + + var builder = new CcuDeviceBuilder() + .WithUri(uri) + .WithApi(api) + .WithAllDevices([parent, channel2, channel1, unrelated]) + .FromDeviceDescription(parent); + + // Act + var ccuDevice = builder.Build(); + + // Assert - only child channels are created and they are ordered by Index. + var channels = ccuDevice.Channels.ToList(); + channels.Should().HaveCount(2); + channels[0].Uri.Address.Should().Be(parentAddress + ":1"); + channels[0].Index.Should().Be(1); + channels[0].ChannelDirection.Should().Be(ChannelDirection.Receiver); + channels[1].Uri.Address.Should().Be(parentAddress + ":2"); + channels[1].Index.Should().Be(2); + channels[1].ChannelDirection.Should().Be(ChannelDirection.Sender); + channels.Should().AllSatisfy(c => c.Uri.CcuHost.Should().Be("localhost")); + channels.Should().AllSatisfy(c => c.Uri.CcuName.Should().Be("ccu")); + channels.Should().AllSatisfy(c => c.Uri.Kind.Should().Be(CcuDeviceKind.HomeMatic)); + } + [Fact] public void Build_WithNullDeviceDescription_ReturnsCcuDeviceWithDefaultProperties() { @@ -142,4 +249,53 @@ public void Build_WithNullDeviceDescription_ReturnsCcuDeviceWithDefaultPropertie ccuDevice.Roaming.Should().BeFalse(); ccuDevice.ParamSets.Should().BeEmpty(); } + + [Fact] + public void Build_WithDeviceDescription_UsesCaseInsensitiveParentMatchingForChannels() + { + // Arrange - parent uses upper case, child references lower case parent address. + var parent = new DeviceDescription + { + Address = "PARENT1", + Parent = string.Empty, + DeviceType = "ParentType", + ParamSets = [] + }; + + var child = new DeviceDescription + { + Address = "PARENT1:1", + Parent = "parent1", + DeviceType = "ChannelType", + Index = 1, + Group = string.Empty, + ChannelDirection = ChannelDirection.Receiver, + Interface = "BidCos-RF", + Version = 1, + IsAesActive = false, + Roaming = false, + ParamSets = [] + }; + + var api = A.Fake(); + var uri = new CcuDeviceUri + { + CcuHost = "localhost", + Kind = CcuDeviceKind.HomeMatic, + Address = "PARENT1" + }; + + var builder = new CcuDeviceBuilder() + .WithUri(uri) + .WithApi(api) + .WithAllDevices([parent, child]) + .FromDeviceDescription(parent); + + // Act + var ccuDevice = builder.Build(); + + // Assert + ccuDevice.Channels.Should().ContainSingle() + .Which.Uri.Address.Should().Be("PARENT1:1"); + } } diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceTests.cs new file mode 100644 index 0000000..f54c0cc --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceTests.cs @@ -0,0 +1,107 @@ +using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.Core.Devices; +using CreativeCoders.HomeMatic.XmlRpc; +using CreativeCoders.HomeMatic.XmlRpc.Client; +using CreativeCoders.HomeMatic.XmlRpc.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; +using FakeItEasy; +using AwesomeAssertions; + +namespace CreativeCoders.HomeMatic.Tests; + +public class CcuDeviceTests +{ + [Fact] + public async Task GetChannelAsync_WithMatchingAddress_ReturnsChannel() + { + // Arrange + var api = A.Fake(); + var channel1 = CreateChannel(api, "DEV:1", 1); + var channel2 = CreateChannel(api, "DEV:2", 2); + + var device = CreateDevice(api, [channel1, channel2]); + + // Act + var result = await device.GetChannelAsync("DEV:2"); + + // Assert + result.Should().BeSameAs(channel2); + } + + [Fact] + public async Task GetChannelAsync_WithUnknownAddress_ThrowsKeyNotFoundException() + { + // Arrange + var api = A.Fake(); + var device = CreateDevice(api, [CreateChannel(api, "DEV:1", 1)]); + + // Act + var act = () => device.GetChannelAsync("DEV:UNKNOWN"); + + // Assert + await act.Should().ThrowAsync() + .WithMessage("Channel with address 'DEV:UNKNOWN' not found."); + } + + [Fact] + public async Task GetChannelAsync_WithNoChannels_ThrowsKeyNotFoundException() + { + // Arrange + var api = A.Fake(); + var device = CreateDevice(api, []); + + // Act + var act = () => device.GetChannelAsync("DEV:1"); + + // Assert + await act.Should().ThrowAsync(); + } + + private static CcuDevice CreateDevice(IHomeMaticXmlRpcApi api, IEnumerable channels) + { + return new CcuDevice(api) + { + Uri = new CcuDeviceUri + { + CcuHost = "localhost", + Kind = CcuDeviceKind.HomeMatic, + Address = "DEV" + }, + DeviceType = "TestType", + IsAesActive = false, + Interface = "BidCos-RF", + Version = 1, + Roaming = false, + ParamSets = [], + RxMode = RxModes.Always, + RfAddress = 0, + Firmware = "1.0.0", + AvailableFirmware = "1.0.0", + CanBeUpdated = false, + FirmwareUpdateState = DeviceFirmwareUpdateState.None, + Channels = channels + }; + } + + private static CcuDeviceChannel CreateChannel(IHomeMaticXmlRpcApi api, string address, int index) + { + return new CcuDeviceChannel(api) + { + Uri = new CcuDeviceUri + { + CcuHost = "localhost", + Kind = CcuDeviceKind.HomeMatic, + Address = address + }, + DeviceType = "ChannelType", + IsAesActive = false, + Interface = "BidCos-RF", + Version = 1, + Roaming = false, + ParamSets = [], + Index = index, + Group = string.Empty, + ChannelDirection = ChannelDirection.Receiver + }; + } +} diff --git a/tests/CreativeCoders.HomeMatic.Tests/CompleteCcuDeviceBuilderTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CompleteCcuDeviceBuilderTests.cs new file mode 100644 index 0000000..6774b05 --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/CompleteCcuDeviceBuilderTests.cs @@ -0,0 +1,215 @@ +using CreativeCoders.HomeMatic.Core.Devices; +using FakeItEasy; +using AwesomeAssertions; + +namespace CreativeCoders.HomeMatic.Tests; + +public class CompleteCcuDeviceBuilderTests +{ + [Fact] + public async Task BuildAsync_WithDeviceAndChannels_ReturnsCompleteDeviceWithParamSetValues() + { + // Arrange + var device = A.Fake(); + var channel = A.Fake(); + + A.CallTo(() => device.Channels).Returns([channel]); + A.CallTo(() => device.ParamSets).Returns(["MASTER"]); + A.CallTo(() => channel.ParamSets).Returns(["VALUES"]); + + SetupParamSet(device, "MASTER", "AES_ACTIVE", true); + SetupParamSet(channel, "VALUES", "STATE", false); + + var builder = new CompleteCcuDeviceBuilder(); + + // Act + var completeDevice = await builder.BuildAsync(device); + + // Assert + completeDevice.DeviceData.Should().BeSameAs(device); + + var channels = completeDevice.Channels.ToList(); + channels.Should().HaveCount(1); + channels[0].ChannelData.Should().BeSameAs(channel); + + var channelParamSets = channels[0].ParamSetValues.ToList(); + channelParamSets.Should().HaveCount(1); + channelParamSets[0].ParamSetKey.Should().Be("VALUES"); + channelParamSets[0].ParamSetValues.Should().ContainSingle() + .Which.ParamSetValue.Name.Should().Be("STATE"); + + var deviceParamSets = completeDevice.ParamSetValues.ToList(); + deviceParamSets.Should().HaveCount(1); + deviceParamSets[0].ParamSetKey.Should().Be("MASTER"); + deviceParamSets[0].ParamSetValues.Should().ContainSingle() + .Which.ParamSetValue.Name.Should().Be("AES_ACTIVE"); + } + + [Fact] + public async Task BuildAsync_SkipsLinkParamSetKey() + { + // Arrange + var device = A.Fake(); + + A.CallTo(() => device.Channels).Returns([]); + A.CallTo(() => device.ParamSets).Returns(["MASTER", "LINK", "VALUES"]); + + SetupParamSet(device, "MASTER", "A", 1); + SetupParamSet(device, "VALUES", "B", 2); + + var builder = new CompleteCcuDeviceBuilder(); + + // Act + var completeDevice = await builder.BuildAsync(device); + + // Assert - LINK must not be requested because it is filtered out. + A.CallTo(() => device.GetParamSetValuesAsync("LINK")).MustNotHaveHappened(); + A.CallTo(() => device.GetParamSetDescriptionsAsync("LINK")).MustNotHaveHappened(); + + completeDevice.ParamSetValues.Select(x => x.ParamSetKey) + .Should().BeEquivalentTo("MASTER", "VALUES"); + } + + [Fact] + public async Task BuildAsync_WhenValueHasNoMatchingDescription_ThrowsKeyNotFoundException() + { + // Arrange + var device = A.Fake(); + + A.CallTo(() => device.Channels).Returns([]); + A.CallTo(() => device.ParamSets).Returns(["MASTER"]); + + // Values contain "A", but the description list is empty -> FirstOrDefault returns null -> throw. + A.CallTo(() => device.GetParamSetValuesAsync("MASTER")) + .Returns(Task.FromResult>( + [ + new ParamSetValue { Name = "A", Value = 1 } + ])); + + A.CallTo(() => device.GetParamSetDescriptionsAsync("MASTER")) + .Returns(Task.FromResult(new CcuParameterDescriptions + { + ParamSetKey = "MASTER", + Items = [] + })); + + var builder = new CompleteCcuDeviceBuilder(); + + // Act + var act = async () => + { + var result = await builder.BuildAsync(device); + // ParamSetValues uses deferred execution via Select; force enumeration. + foreach (var paramSet in result.ParamSetValues) + { + _ = paramSet.ParamSetValues.ToList(); + } + }; + + // Assert + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task BuildAsync_WithEmptyParamSets_ReturnsEmptyParamSetValues() + { + // Arrange + var device = A.Fake(); + A.CallTo(() => device.Channels).Returns([]); + A.CallTo(() => device.ParamSets).Returns([]); + + var builder = new CompleteCcuDeviceBuilder(); + + // Act + var completeDevice = await builder.BuildAsync(device); + + // Assert + completeDevice.ParamSetValues.Should().BeEmpty(); + completeDevice.Channels.Should().BeEmpty(); + } + + [Fact] + public async Task BuildAsync_WithMultipleChannelsEachHavingOwnParamSets_MapsEachChannelIndependently() + { + // Arrange + var device = A.Fake(); + var channelA = A.Fake(); + var channelB = A.Fake(); + + A.CallTo(() => device.Channels).Returns([channelA, channelB]); + A.CallTo(() => device.ParamSets).Returns([]); + A.CallTo(() => channelA.ParamSets).Returns(["VALUES"]); + A.CallTo(() => channelB.ParamSets).Returns(["MASTER"]); + + SetupParamSet(channelA, "VALUES", "LEVEL", 50); + SetupParamSet(channelB, "MASTER", "AES_ACTIVE", true); + + var builder = new CompleteCcuDeviceBuilder(); + + // Act + var completeDevice = await builder.BuildAsync(device); + + // Assert + var channels = completeDevice.Channels.ToList(); + channels.Should().HaveCount(2); + channels[0].ChannelData.Should().BeSameAs(channelA); + channels[0].ParamSetValues.Should().ContainSingle() + .Which.ParamSetKey.Should().Be("VALUES"); + channels[1].ChannelData.Should().BeSameAs(channelB); + channels[1].ParamSetValues.Should().ContainSingle() + .Which.ParamSetKey.Should().Be("MASTER"); + } + + [Fact] + public async Task BuildAsync_ChannelWithoutParamSets_ReturnsChannelWithEmptyParamSetValues() + { + // Arrange + var device = A.Fake(); + var channel = A.Fake(); + + A.CallTo(() => device.Channels).Returns([channel]); + A.CallTo(() => device.ParamSets).Returns([]); + A.CallTo(() => channel.ParamSets).Returns([]); + + var builder = new CompleteCcuDeviceBuilder(); + + // Act + var completeDevice = await builder.BuildAsync(device); + + // Assert + completeDevice.Channels.Should().ContainSingle() + .Which.ParamSetValues.Should().BeEmpty(); + } + + private static void SetupParamSet(ICcuDeviceBase device, string paramSetKey, string name, object value) + { + A.CallTo(() => device.GetParamSetValuesAsync(paramSetKey)) + .Returns(Task.FromResult>( + [ + new ParamSetValue { Name = name, Value = value } + ])); + + A.CallTo(() => device.GetParamSetDescriptionsAsync(paramSetKey)) + .Returns(Task.FromResult(new CcuParameterDescriptions + { + ParamSetKey = paramSetKey, + Items = + [ + new CcuParameterDescription + { + Id = name, + DefaultValue = null, + MinValue = null, + MaxValue = null, + Type = null, + DataType = default, + Unit = null, + TabOrder = 0, + Control = null, + ValuesList = [], + SpecialValues = [] + } + ] + })); + } +} diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs index e333e53..7798c33 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs @@ -3,7 +3,6 @@ using CreativeCoders.HomeMatic.Core.Devices; using CreativeCoders.HomeMatic.Exporting; using CreativeCoders.HomeMatic.XmlRpc; -using CreativeCoders.HomeMatic.XmlRpc.Devices; using CreativeCoders.HomeMatic.XmlRpc.Parameters; using FakeItEasy; using AwesomeAssertions; diff --git a/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientFactoryTests.cs b/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientFactoryTests.cs new file mode 100644 index 0000000..96a9484 --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientFactoryTests.cs @@ -0,0 +1,79 @@ +using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.XmlRpc; +using FakeItEasy; +using AwesomeAssertions; + +namespace CreativeCoders.HomeMatic.Tests; + +public class MultiCcuClientFactoryTests +{ + [Fact] + public void AddCcu_CallsClientFactoryWithProvidedArguments() + { + // Arrange + var ccuClientFactory = A.Fake(); + var factory = new MultiCcuClientFactory(ccuClientFactory); + var deviceKinds = new[] { CcuDeviceKind.HomeMatic, CcuDeviceKind.HomeMaticIp }; + + // Act + factory.AddCcu("ccu1", "example.com", "admin", "secret", deviceKinds); + + // Assert + A.CallTo(() => ccuClientFactory.CreateClient("ccu1", + A>.That.IsSameSequenceAs(deviceKinds), + "example.com", "admin", "secret")) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public void AddCcu_ReturnsSameFactoryInstanceForFluentChaining() + { + // Arrange + var ccuClientFactory = A.Fake(); + var factory = new MultiCcuClientFactory(ccuClientFactory); + + // Act + var result = factory.AddCcu("ccu1", "host", "user", "pwd", CcuDeviceKind.HomeMatic); + + // Assert + result.Should().BeSameAs(factory); + } + + [Fact] + public void Build_WithoutAddedCcus_ReturnsMultiCcuClient() + { + // Arrange + var ccuClientFactory = A.Fake(); + var factory = new MultiCcuClientFactory(ccuClientFactory); + + // Act + var client = factory.Build(); + + // Assert + client.Should().NotBeNull(); + client.Should().BeOfType(); + } + + [Fact] + public void Build_AfterAddCcu_ReturnsMultiCcuClientConfiguredWithCreatedClients() + { + // Arrange + var ccuClientFactory = A.Fake(); + var createdClient = A.Fake(); + A.CallTo(() => ccuClientFactory.CreateClient( + A._, A>._, A._, A._, A._)) + .Returns(createdClient); + + var factory = new MultiCcuClientFactory(ccuClientFactory) + .AddCcu("ccu1", "host", "user", "pwd", CcuDeviceKind.HomeMatic); + + // Act + var client = factory.Build(); + + // Assert - AddCcu uses the factory; Build constructs a MultiCcuClient. + client.Should().BeOfType(); + A.CallTo(() => ccuClientFactory.CreateClient( + "ccu1", A>._, "host", "user", "pwd")) + .MustHaveHappenedOnceExactly(); + } +} diff --git a/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs b/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs index 3f192dd..f1c8ab9 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/MultiCcuClientTests.cs @@ -126,6 +126,196 @@ public async Task GetCompleteDeviceAsync_SecondCall_UsesOnlyOwningClient() A.CallTo(() => clientB.GetCompleteDeviceAsync(DeviceA)).MustHaveHappenedTwiceExactly(); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task GetCompleteDeviceAsync_NullOrWhitespaceAddress_ThrowsArgumentException(string? address) + { + var multi = new MultiCcuClient([CreateClientWithCompleteDevices()], new CcuRoutingTable()); + + var act = () => multi.GetCompleteDeviceAsync(address!); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task GetCompleteDeviceAsync_UnknownDevice_ProbesAllClientsAndThrows() + { + var clientA = CreateClientWithCompleteDevices(); + var clientB = CreateClientWithCompleteDevices(); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + var act = () => multi.GetCompleteDeviceAsync("UNKNOWN"); + + await act.Should().ThrowAsync(); + A.CallTo(() => clientA.GetCompleteDeviceAsync("UNKNOWN")).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetCompleteDeviceAsync("UNKNOWN")).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task GetCompleteDeviceAsync_ChannelAddress_UsesDeviceLevelRoute() + { + var clientA = CreateClientWithCompleteDevices(DeviceA); + var clientB = CreateClientWithCompleteDevices(DeviceB); + + var channelDevice = A.Fake(); + A.CallTo(() => clientB.GetCompleteDeviceAsync($"{DeviceB}:1")) + .Returns(Task.FromResult(channelDevice)); + A.CallTo(() => clientA.GetCompleteDeviceAsync($"{DeviceB}:1")) + .ThrowsAsync(new KeyNotFoundException()); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + // Populate routing via the bulk call. + _ = await multi.GetCompleteDevicesAsync(); + + var result = await multi.GetCompleteDeviceAsync($"{DeviceB}:1"); + + result.Should().BeSameAs(channelDevice); + A.CallTo(() => clientA.GetCompleteDeviceAsync($"{DeviceB}:1")).MustNotHaveHappened(); + A.CallTo(() => clientB.GetCompleteDeviceAsync($"{DeviceB}:1")).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task GetCompleteDeviceAsync_CachedClientFails_FallsBackToOtherClientsAndUpdatesRoute() + { + var clientA = CreateClientWithCompleteDevices(DeviceA); + var clientB = CreateClientWithCompleteDevices(DeviceA); + + var routingTable = new CcuRoutingTable(); + // Pre-seed stale mapping to clientA, and make clientA throw to simulate stale entry. + A.CallTo(() => clientA.GetCompleteDeviceAsync(DeviceA)).ThrowsAsync(new KeyNotFoundException()); + routingTable.Register(DeviceA, clientA); + + var multi = new MultiCcuClient([clientA, clientB], routingTable); + + var device = await multi.GetCompleteDeviceAsync(DeviceA); + + device.Should().NotBeNull(); + A.CallTo(() => clientA.GetCompleteDeviceAsync(DeviceA)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetCompleteDeviceAsync(DeviceA)).MustHaveHappenedOnceExactly(); + + routingTable.TryGetClient(DeviceA, out var resolved).Should().BeTrue(); + resolved.Should().BeSameAs(clientB); + } + + [Fact] + public async Task GetCompleteDevicesAsync_ReturnsAggregatedDevicesFromAllClients() + { + var clientA = CreateClientWithCompleteDevices(DeviceA); + var clientB = CreateClientWithCompleteDevices(DeviceB); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + var devices = (await multi.GetCompleteDevicesAsync()).ToList(); + + devices.Should().HaveCount(2); + devices.Select(d => d.DeviceData.Uri.Address).Should().BeEquivalentTo(DeviceA, DeviceB); + } + + [Fact] + public async Task GetCompleteDevicesAsync_PopulatesRoutingTableSoSubsequentGetCompleteDeviceSkipsOtherClients() + { + var clientA = CreateClientWithCompleteDevices(DeviceA); + var clientB = CreateClientWithCompleteDevices(DeviceB); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + _ = await multi.GetCompleteDevicesAsync(); + + await multi.GetCompleteDeviceAsync(DeviceA); + await multi.GetCompleteDeviceAsync(DeviceB); + + A.CallTo(() => clientA.GetCompleteDeviceAsync(DeviceA)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientA.GetCompleteDeviceAsync(DeviceB)).MustNotHaveHappened(); + A.CallTo(() => clientB.GetCompleteDeviceAsync(DeviceB)).MustHaveHappenedOnceExactly(); + A.CallTo(() => clientB.GetCompleteDeviceAsync(DeviceA)).MustNotHaveHappened(); + } + + [Fact] + public async Task GetCompleteDevicesAsync_DeviceWithEmptyAddress_IsNotRegisteredInRoutingTable() + { + // One client returns a device with an empty address; the routing table must skip that entry. + var clientA = A.Fake(); + var deviceData = A.Fake(); + A.CallTo(() => deviceData.Uri).Returns(new CcuDeviceUri + { + CcuHost = "ccu", Kind = CcuDeviceKind.HomeMatic, Address = string.Empty + }); + var device = A.Fake(); + A.CallTo(() => device.DeviceData).Returns(deviceData); + A.CallTo(() => clientA.GetCompleteDevicesAsync()) + .Returns(Task.FromResult>([device])); + + var routingTable = A.Fake(); + var multi = new MultiCcuClient([clientA], routingTable); + + var devices = (await multi.GetCompleteDevicesAsync()).ToList(); + + devices.Should().ContainSingle().Which.Should().BeSameAs(device); + A.CallTo(() => routingTable.Register( + A>>.That.Matches(entries => !entries.Any()))) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task GetCompleteDevicesAsync_NoClients_ReturnsEmpty() + { + var multi = new MultiCcuClient([], new CcuRoutingTable()); + + var devices = await multi.GetCompleteDevicesAsync(); + + devices.Should().BeEmpty(); + } + + [Fact] + public async Task GetCompleteDeviceAsync_NoClients_ThrowsKeyNotFoundException() + { + var multi = new MultiCcuClient([], new CcuRoutingTable()); + + var act = () => multi.GetCompleteDeviceAsync(DeviceA); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task GetCompleteDeviceAsync_CachedClientThrowsNonKeyNotFound_PropagatesWithoutFallback() + { + // The cached client throws an unexpected exception. The fallback must not swallow it and + // the other clients must not be probed. + var clientA = CreateClientWithCompleteDevices(); + var clientB = CreateClientWithCompleteDevices(DeviceA); + A.CallTo(() => clientA.GetCompleteDeviceAsync(DeviceA)).ThrowsAsync(new InvalidOperationException("net")); + + var routingTable = new CcuRoutingTable(); + routingTable.Register(DeviceA, clientA); + + var multi = new MultiCcuClient([clientA, clientB], routingTable); + + var act = () => multi.GetCompleteDeviceAsync(DeviceA); + + await act.Should().ThrowAsync().WithMessage("net"); + A.CallTo(() => clientB.GetCompleteDeviceAsync(DeviceA)).MustNotHaveHappened(); + } + + [Fact] + public async Task GetCompleteDeviceAsync_ProbingClientThrowsNonKeyNotFound_Propagates() + { + // Only KeyNotFoundException should trigger the probe fallback; other exceptions must bubble up. + var clientA = A.Fake(); + var clientB = CreateClientWithCompleteDevices(DeviceA); + A.CallTo(() => clientA.GetCompleteDeviceAsync(DeviceA)).ThrowsAsync(new InvalidOperationException("net")); + + var multi = new MultiCcuClient([clientA, clientB], new CcuRoutingTable()); + + var act = () => multi.GetCompleteDeviceAsync(DeviceA); + + await act.Should().ThrowAsync().WithMessage("net"); + A.CallTo(() => clientB.GetCompleteDeviceAsync(DeviceA)).MustNotHaveHappened(); + } + private static ICcuClient CreateClientWithDevices(params string[] addresses) { var client = A.Fake(); diff --git a/tests/CreativeCoders.HomeMatic.Tests/XmlRpcApiConnectionTests.cs b/tests/CreativeCoders.HomeMatic.Tests/XmlRpcApiConnectionTests.cs new file mode 100644 index 0000000..3ecb0d9 --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/XmlRpcApiConnectionTests.cs @@ -0,0 +1,66 @@ +using CreativeCoders.HomeMatic.XmlRpc; +using CreativeCoders.HomeMatic.XmlRpc.Client; +using FakeItEasy; +using AwesomeAssertions; + +namespace CreativeCoders.HomeMatic.Tests; + +public class XmlRpcApiConnectionTests +{ + [Fact] + public void Ctor_WithValidArguments_AssignsProperties() + { + // Arrange + var address = new XmlRpcApiAddress(new Uri("http://example.com"), CcuDeviceKind.HomeMatic); + var api = A.Fake(); + + // Act + var connection = new XmlRpcApiConnection(address, api); + + // Assert + connection.Address.Should().BeSameAs(address); + connection.Api.Should().BeSameAs(api); + connection.CcuName.Should().BeEmpty(); + } + + [Fact] + public void Ctor_WithNullAddress_ThrowsArgumentNullException() + { + // Arrange + var api = A.Fake(); + + // Act + Action act = () => _ = new XmlRpcApiConnection(null!, api); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Ctor_WithNullApi_ThrowsArgumentNullException() + { + // Arrange + var address = new XmlRpcApiAddress(new Uri("http://example.com"), CcuDeviceKind.HomeMatic); + + // Act + Action act = () => _ = new XmlRpcApiConnection(address, null!); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void CcuName_CanBeAssigned() + { + // Arrange + var connection = new XmlRpcApiConnection( + new XmlRpcApiAddress(new Uri("http://example.com"), CcuDeviceKind.HomeMatic), + A.Fake()); + + // Act + connection.CcuName = "ccu1"; + + // Assert + connection.CcuName.Should().Be("ccu1"); + } +} From 8bf376de8907295ecaa41e0e6d5c5ebc2494b4fd Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:13:36 +0200 Subject: [PATCH 09/15] feat(exporting): add support for `ParamValueNameWhitelist` in device exports - Introduced `ParamValueNameWhitelist` in `DeviceExportOptions` to filter exported parameter values by name. - Updated `DeviceExporter` to apply name-based filtering in addition to param set filtering. - Enhanced CLI command with whitelist integration and added tests for comprehensive coverage. --- .../Exporting/DeviceExportOptions.cs | 28 ++- .../Exporting/DeviceExporter.cs | 14 +- .../HomeMaticServiceCollectionExtensions.cs | 2 + .../Commanding/JsonDataExporterBase.cs | 6 +- .../Device/Export/ExportDevicesCommand.cs | 10 +- .../Exporting/DeviceExportOptionsTests.cs | 94 +++++++++ .../Exporting/DeviceExporterTests.cs | 188 ++++++++++++++++++ 7 files changed, 333 insertions(+), 9 deletions(-) diff --git a/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs b/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs index 062a780..cdb7ebe 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs @@ -8,18 +8,44 @@ public class DeviceExportOptions /// public ICollection? ParamSetWhitelist { get; set; } + /// + /// Whitelist of ParamSetValue names to include in the export (e.g. "BOOST_TIME", "SET_TEMPERATURE"). + /// If empty or null, all ParamSetValues within allowed ParamSets are exported. + /// + public ICollection? ParamValueNameWhitelist { get; set; } + /// /// Whether to write indented JSON output. /// public bool WriteIndented { get; set; } = true; + /// + /// Determines whether a ParamSet key is allowed based on the . + /// + /// The ParamSet key to check. + /// true if the key is allowed or no whitelist is configured; otherwise false. public bool IsParamSetAllowed(string paramSetKey) { - if (ParamSetWhitelist == null || ParamSetWhitelist.Count == 0) + if (ParamSetWhitelist is null || ParamSetWhitelist.Count == 0) { return true; } return ParamSetWhitelist.Contains(paramSetKey, StringComparer.OrdinalIgnoreCase); } + + /// + /// Determines whether a ParamSetValue name is allowed based on the . + /// + /// The ParamSetValue name to check. + /// true if the name is allowed or no whitelist is configured; otherwise false. + public bool IsParamValueNameAllowed(string paramValueName) + { + if (ParamValueNameWhitelist is null || ParamValueNameWhitelist.Count == 0) + { + return true; + } + + return ParamValueNameWhitelist.Contains(paramValueName, StringComparer.OrdinalIgnoreCase); + } } diff --git a/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs b/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs index ce6ca2e..3ebf381 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs @@ -58,12 +58,14 @@ private static IEnumerable BuildParamSetExportData( .Select(ps => new ParamSetExportData { ParamSetKey = ps.ParamSetKey, - Values = ps.ParamSetValues.Select(v => new ParamValueExportData - { - Key = v.ParamSetValue.Name, - Name = v.Description.Id, - Value = v.ParamSetValue.Value - }).ToList() + Values = ps.ParamSetValues + .Where(v => options?.IsParamValueNameAllowed(v.ParamSetValue.Name) ?? true) + .Select(v => new ParamValueExportData + { + Key = v.ParamSetValue.Name, + Name = v.Description.Id == v.ParamSetValue.Name ? null : v.Description.Id, + Value = v.ParamSetValue.Value + }).ToList() }) .ToList(); } diff --git a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs index dc4014f..c4b0dbf 100644 --- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs +++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.Exporting; using CreativeCoders.HomeMatic.JsonRpc; using CreativeCoders.HomeMatic.XmlRpc; using Microsoft.Extensions.DependencyInjection; @@ -16,6 +17,7 @@ public static IServiceCollection AddHomeMatic(this IServiceCollection services) services.TryAddTransient(); services.TryAddTransient(); services.TryAddTransient(); + services.TryAddSingleton(); return services; } diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/JsonDataExporterBase.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/JsonDataExporterBase.cs index b70afd6..81a072c 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/JsonDataExporterBase.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/JsonDataExporterBase.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; using CreativeCoders.Core.IO; using CreativeCoders.Core.Text; using CreativeCoders.HomeMatic.Core; @@ -26,7 +27,10 @@ public async Task ExportAsync(IMultiCcuClient ccuClient, TOptions options, strin await FileSys.File.WriteAllTextAsync(outputFileName, outputData.ToJson(new JsonSerializerOptions - { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase })) + { + WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + })) .ConfigureAwait(false); } } diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/Export/ExportDevicesCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/Export/ExportDevicesCommand.cs index 7263388..069ca65 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/Export/ExportDevicesCommand.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/Export/ExportDevicesCommand.cs @@ -1,6 +1,8 @@ using CreativeCoders.Cli.Core; +using CreativeCoders.Core; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Core.Devices; +using CreativeCoders.HomeMatic.Exporting; using CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding; using JetBrains.Annotations; using Spectre.Console; @@ -9,11 +11,17 @@ namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Device.Export; [UsedImplicitly] [CliCommand([DeviceCommandGroup.Name, "export"], Description = "Export device to json file")] -public class ExportDevicesCommand(IAnsiConsole console, IMultiCcuClient multiCcuClient) +public class ExportDevicesCommand(IAnsiConsole console, IMultiCcuClient multiCcuClient, IDeviceExporter deviceExporter) : JsonExportCommandBase(console, multiCcuClient) { + private readonly IDeviceExporter _deviceExporter = Ensure.NotNull(deviceExporter); + protected override object TransformData(ICompleteCcuDevice device) { + return _deviceExporter.BuildExportData(device, new DeviceExportOptions + { + WriteIndented = true + }); return new { Device = device.DeviceData, diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs index ef2454d..31529ed 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs @@ -99,6 +99,100 @@ public void IsParamSetAllowed_WithEmptyStringKeyAndEmptyWhitelist_ReturnsTrue() result.Should().BeTrue(); } + [Fact] + public void IsParamValueNameAllowed_WithNullWhitelist_ReturnsTrue() + { + // Arrange + var options = new DeviceExportOptions { ParamValueNameWhitelist = null }; + + // Act + var result = options.IsParamValueNameAllowed("BOOST_TIME"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void IsParamValueNameAllowed_WithEmptyWhitelist_ReturnsTrue() + { + // Arrange + var options = new DeviceExportOptions { ParamValueNameWhitelist = [] }; + + // Act + var result = options.IsParamValueNameAllowed("BOOST_TIME"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void IsParamValueNameAllowed_WithMatchingName_ReturnsTrue() + { + // Arrange + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME", "SET_TEMPERATURE"] }; + + // Act + var result = options.IsParamValueNameAllowed("BOOST_TIME"); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void IsParamValueNameAllowed_WithNonMatchingName_ReturnsFalse() + { + // Arrange + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME", "SET_TEMPERATURE"] }; + + // Act + var result = options.IsParamValueNameAllowed("ACTUAL_TEMPERATURE"); + + // Assert + result.Should().BeFalse(); + } + + [Theory] + [InlineData("boost_time")] + [InlineData("Boost_Time")] + [InlineData("BOOST_TIME")] + public void IsParamValueNameAllowed_WithDifferentCasing_ReturnsTrue(string name) + { + // Arrange + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME"] }; + + // Act + var result = options.IsParamValueNameAllowed(name); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void IsParamValueNameAllowed_WithEmptyStringName_ReturnsFalseWhenNotInWhitelist() + { + // Arrange + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME"] }; + + // Act + var result = options.IsParamValueNameAllowed(string.Empty); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void IsParamValueNameAllowed_WithEmptyStringNameAndEmptyWhitelist_ReturnsTrue() + { + // Arrange + var options = new DeviceExportOptions { ParamValueNameWhitelist = [] }; + + // Act + var result = options.IsParamValueNameAllowed(string.Empty); + + // Assert + result.Should().BeTrue(); + } + [Fact] public void WriteIndented_DefaultValue_IsTrue() { diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs index 7798c33..4649c69 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs @@ -633,4 +633,192 @@ public void BuildExportData_WithEmptyParamSetInParamSetValues_ReturnsParamSetWit result.ParamSetValues.Should().HaveCount(1); result.ParamSetValues.First().Values.Should().BeEmpty(); } + + [Fact] + public void BuildExportData_WithParamValueNameWhitelist_FiltersOutNonWhitelistedValues() + { + // Arrange + var paramSetValues = new[] + { + CreateParamSetValues("MASTER", + ("BOOST_TIME", 5, "Boost Time"), + ("DECALCIFICATION_TIME", 22, "Decalcification Time"), + ("PARTY_MODE", false, "Party Mode")) + }; + var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME", "PARTY_MODE"] }; + var sut = new DeviceExporter(); + + // Act + var result = sut.BuildExportData(device, options); + + // Assert + result.ParamSetValues.Should().HaveCount(1); + var values = result.ParamSetValues.First().Values.ToList(); + values.Should().HaveCount(2); + values.Select(v => v.Key).Should().BeEquivalentTo("BOOST_TIME", "PARTY_MODE"); + } + + [Fact] + public void BuildExportData_WithParamValueNameWhitelistNonMatching_ReturnsEmptyValues() + { + // Arrange + var paramSetValues = new[] + { + CreateParamSetValues("VALUES", ("SET_TEMPERATURE", 21.5, "Set Temperature")) + }; + var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["NONEXISTENT"] }; + var sut = new DeviceExporter(); + + // Act + var result = sut.BuildExportData(device, options); + + // Assert + result.ParamSetValues.Should().HaveCount(1); + result.ParamSetValues.First().Values.Should().BeEmpty(); + } + + [Fact] + public void BuildExportData_WithNullParamValueNameWhitelist_IncludesAllValues() + { + // Arrange + var paramSetValues = new[] + { + CreateParamSetValues("MASTER", + ("BOOST_TIME", 5, "Boost Time"), + ("DECALCIFICATION_TIME", 22, "Decalcification Time")) + }; + var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var options = new DeviceExportOptions { ParamValueNameWhitelist = null }; + var sut = new DeviceExporter(); + + // Act + var result = sut.BuildExportData(device, options); + + // Assert + result.ParamSetValues.First().Values.Should().HaveCount(2); + } + + [Fact] + public void BuildExportData_WithEmptyParamValueNameWhitelist_IncludesAllValues() + { + // Arrange + var paramSetValues = new[] + { + CreateParamSetValues("MASTER", + ("BOOST_TIME", 5, "Boost Time"), + ("DECALCIFICATION_TIME", 22, "Decalcification Time")) + }; + var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var options = new DeviceExportOptions { ParamValueNameWhitelist = [] }; + var sut = new DeviceExporter(); + + // Act + var result = sut.BuildExportData(device, options); + + // Assert + result.ParamSetValues.First().Values.Should().HaveCount(2); + } + + [Fact] + public void BuildExportData_WithBothWhitelists_StacksFilters() + { + // Arrange – two ParamSets with different values each + var paramSetValues = new[] + { + CreateParamSetValues("MASTER", + ("BOOST_TIME", 5, "Boost Time"), + ("DECALCIFICATION_TIME", 22, "Decalcification Time")), + CreateParamSetValues("LINK", + ("PEER_NEEDS_BURST", true, "Peer Needs Burst")) + }; + var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + // Allow only MASTER ParamSet and only BOOST_TIME value + var options = new DeviceExportOptions + { + ParamSetWhitelist = ["MASTER"], + ParamValueNameWhitelist = ["BOOST_TIME"] + }; + var sut = new DeviceExporter(); + + // Act + var result = sut.BuildExportData(device, options); + + // Assert – LINK is filtered out by ParamSetWhitelist, DECALCIFICATION_TIME by ParamValueNameWhitelist + result.ParamSetValues.Should().HaveCount(1); + result.ParamSetValues.First().ParamSetKey.Should().Be("MASTER"); + result.ParamSetValues.First().Values.Should().HaveCount(1); + result.ParamSetValues.First().Values.First().Key.Should().Be("BOOST_TIME"); + } + + [Fact] + public void BuildExportData_WithParamValueNameWhitelistCaseInsensitive_FiltersCorrectly() + { + // Arrange + var paramSetValues = new[] + { + CreateParamSetValues("VALUES", + ("SET_TEMPERATURE", 21.5, "Set Temperature"), + ("ACTUAL_TEMPERATURE", 20.1, "Actual Temperature")) + }; + var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["set_temperature"] }; + var sut = new DeviceExporter(); + + // Act + var result = sut.BuildExportData(device, options); + + // Assert + result.ParamSetValues.First().Values.Should().HaveCount(1); + result.ParamSetValues.First().Values.First().Key.Should().Be("SET_TEMPERATURE"); + } + + [Fact] + public void BuildExportData_WithChannelAndParamValueNameWhitelist_FiltersChannelValues() + { + // Arrange + var channelParamSetValues = new[] + { + CreateParamSetValues("VALUES", + ("SET_TEMPERATURE", 21.5, "Set Temperature"), + ("ACTUAL_TEMPERATURE", 20.1, "Actual Temperature"), + ("VALVE_STATE", 45, "Valve State")) + }; + var channel = CreateFakeChannel(paramSetValues: channelParamSetValues); + var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [channel] }); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["SET_TEMPERATURE", "VALVE_STATE"] }; + var sut = new DeviceExporter(); + + // Act + var result = sut.BuildExportData(device, options); + + // Assert + var channelResult = result.Channels.First(); + channelResult.ParamSetValues.First().Values.Should().HaveCount(2); + channelResult.ParamSetValues.First().Values.Select(v => v.Key) + .Should().BeEquivalentTo("SET_TEMPERATURE", "VALVE_STATE"); + } + + [Fact] + public async Task ExportDevicesAsync_WithParamValueNameWhitelist_FiltersValuesPerDevice() + { + // Arrange + var paramSetValues = new[] + { + CreateParamSetValues("MASTER", + ("BOOST_TIME", 5, "Boost Time"), + ("DECALCIFICATION_TIME", 22, "Decalcification Time")) + }; + var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME"] }; + var sut = new DeviceExporter(); + + // Act + var json = await sut.ExportDevicesAsync([device], options); + + // Assert + json.Should().Contain("BOOST_TIME"); + json.Should().NotContain("DECALCIFICATION_TIME"); + } } From 056539b5368215388fbb5ed266b4c0f1edd1cd5c Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:27:52 +0200 Subject: [PATCH 10/15] feat(tests): add builders for creating fake HomeMatic devices and channels - Introduced `CompleteCcuDeviceFakeBuilder` and `CompleteCcuDeviceChannelFakeBuilder` for streamlined test data creation. - Added `ParamSetValuesBuilder` to simplify parameter set construction. - Updated test suite to replace manual fake creation with new builders. - Refactored tests for `DeviceExporter` and `DeviceExportOptions` for improved readability and maintainability. --- .../CompleteCcuDeviceChannelFakeBuilder.cs | 83 ++ .../Exporting/CompleteCcuDeviceFakeBuilder.cs | 109 +++ .../Exporting/DeviceExportOptionsTests.cs | 200 +---- .../Exporting/DeviceExporterTests.cs | 792 ++++++++---------- .../Exporting/ParamSetValuesBuilder.cs | 38 + 5 files changed, 612 insertions(+), 610 deletions(-) create mode 100644 tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceChannelFakeBuilder.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceFakeBuilder.cs create mode 100644 tests/CreativeCoders.HomeMatic.Tests/Exporting/ParamSetValuesBuilder.cs diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceChannelFakeBuilder.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceChannelFakeBuilder.cs new file mode 100644 index 0000000..3b9a9a7 --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceChannelFakeBuilder.cs @@ -0,0 +1,83 @@ +using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.Core.Devices; +using CreativeCoders.HomeMatic.XmlRpc; +using FakeItEasy; + +namespace CreativeCoders.HomeMatic.Tests.Exporting; + +internal sealed class CompleteCcuDeviceChannelFakeBuilder +{ + private string _address = "ABC123456:1"; + private string _deviceType = "HM-CC-RT-DN:01"; + private int _index = 1; + private string[] _paramSets = ["VALUES"]; + private readonly List _paramSetValues = []; + private string _ccuHost = "ccu2.local"; + + public CompleteCcuDeviceChannelFakeBuilder WithAddress(string address) + { + _address = address; + return this; + } + + public CompleteCcuDeviceChannelFakeBuilder WithDeviceType(string deviceType) + { + _deviceType = deviceType; + return this; + } + + public CompleteCcuDeviceChannelFakeBuilder WithIndex(int index) + { + _index = index; + return this; + } + + public CompleteCcuDeviceChannelFakeBuilder WithParamSets(params string[] paramSets) + { + _paramSets = paramSets; + return this; + } + + public CompleteCcuDeviceChannelFakeBuilder WithCcuHost(string ccuHost) + { + _ccuHost = ccuHost; + return this; + } + + public CompleteCcuDeviceChannelFakeBuilder WithParamSet(string paramSetKey, Action? configure = null) + { + var builder = new ParamSetValuesBuilder(); + configure?.Invoke(builder); + + _paramSetValues.Add(new ParamSetValuesWithDescriptions + { + ParamSetKey = paramSetKey, + ParamSetValues = builder.Build().ToList() + }); + + return this; + } + + public ICompleteCcuDeviceChannel Build() + { + var channel = A.Fake(); + var channelData = A.Fake(); + + var uri = new CcuDeviceUri + { + CcuHost = _ccuHost, + Kind = CcuDeviceKind.HomeMatic, + Address = _address + }; + + A.CallTo(() => channelData.Uri).Returns(uri); + A.CallTo(() => channelData.DeviceType).Returns(_deviceType); + A.CallTo(() => channelData.Index).Returns(_index); + A.CallTo(() => channelData.ParamSets).Returns(_paramSets); + + A.CallTo(() => channel.ChannelData).Returns(channelData); + A.CallTo(() => channel.ParamSetValues).Returns(_paramSetValues); + + return channel; + } +} diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceFakeBuilder.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceFakeBuilder.cs new file mode 100644 index 0000000..ed15345 --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/CompleteCcuDeviceFakeBuilder.cs @@ -0,0 +1,109 @@ +using CreativeCoders.HomeMatic.Core; +using CreativeCoders.HomeMatic.Core.Devices; +using CreativeCoders.HomeMatic.XmlRpc; +using FakeItEasy; + +namespace CreativeCoders.HomeMatic.Tests.Exporting; + +internal sealed class CompleteCcuDeviceFakeBuilder +{ + private string _name = "TestDevice"; + private string _address = "ABC123456"; + private string _deviceType = "HM-CC-RT-DN"; + private string _firmware = "1.4"; + private string _ccuHost = "ccu2.local"; + private string _ccuName = string.Empty; + private string[] _paramSetKeys = ["MASTER", "VALUES"]; + private readonly List _paramSetValues = []; + private readonly List _channels = []; + + public CompleteCcuDeviceFakeBuilder WithName(string name) + { + _name = name; + return this; + } + + public CompleteCcuDeviceFakeBuilder WithAddress(string address) + { + _address = address; + return this; + } + + public CompleteCcuDeviceFakeBuilder WithDeviceType(string deviceType) + { + _deviceType = deviceType; + return this; + } + + public CompleteCcuDeviceFakeBuilder WithFirmware(string firmware) + { + _firmware = firmware; + return this; + } + + public CompleteCcuDeviceFakeBuilder WithCcuHost(string ccuHost) + { + _ccuHost = ccuHost; + return this; + } + + public CompleteCcuDeviceFakeBuilder WithCcuName(string ccuName) + { + _ccuName = ccuName; + return this; + } + + public CompleteCcuDeviceFakeBuilder WithParamSetKeys(params string[] paramSetKeys) + { + _paramSetKeys = paramSetKeys; + return this; + } + + public CompleteCcuDeviceFakeBuilder WithParamSet(string paramSetKey, Action? configure = null) + { + var builder = new ParamSetValuesBuilder(); + configure?.Invoke(builder); + + _paramSetValues.Add(new ParamSetValuesWithDescriptions + { + ParamSetKey = paramSetKey, + ParamSetValues = builder.Build().ToList() + }); + + return this; + } + + public CompleteCcuDeviceFakeBuilder WithChannel(Action? configure = null) + { + var builder = new CompleteCcuDeviceChannelFakeBuilder().WithCcuHost(_ccuHost); + configure?.Invoke(builder); + _channels.Add(builder.Build()); + return this; + } + + public ICompleteCcuDevice Build() + { + var device = A.Fake(); + var deviceData = A.Fake(); + + var uri = new CcuDeviceUri + { + CcuHost = _ccuHost, + CcuName = _ccuName, + Kind = CcuDeviceKind.HomeMatic, + Address = _address + }; + + A.CallTo(() => deviceData.Name).Returns(_name); + A.CallTo(() => deviceData.Uri).Returns(uri); + A.CallTo(() => deviceData.DeviceType).Returns(_deviceType); + A.CallTo(() => deviceData.Firmware).Returns(_firmware); + A.CallTo(() => deviceData.ParamSets).Returns(_paramSetKeys); + + A.CallTo(() => device.DeviceData).Returns(deviceData); + A.CallTo(() => device.ParamSetValues).Returns(_paramSetValues); + A.CallTo(() => device.Channels).Returns(_channels); + + return device; + } +} diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs index 31529ed..5181c28 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExportOptionsTests.cs @@ -1,205 +1,95 @@ -using CreativeCoders.HomeMatic.Exporting; using AwesomeAssertions; +using CreativeCoders.HomeMatic.Exporting; namespace CreativeCoders.HomeMatic.Tests.Exporting; public class DeviceExportOptionsTests { - [Fact] - public void IsParamSetAllowed_WithNullWhitelist_ReturnsTrue() - { - // Arrange - var options = new DeviceExportOptions { ParamSetWhitelist = null }; - - // Act - var result = options.IsParamSetAllowed("MASTER"); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void IsParamSetAllowed_WithEmptyWhitelist_ReturnsTrue() - { - // Arrange - var options = new DeviceExportOptions { ParamSetWhitelist = [] }; - - // Act - var result = options.IsParamSetAllowed("MASTER"); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void IsParamSetAllowed_WithMatchingKey_ReturnsTrue() + public static TheoryData ParamSetAllowedCases => new() { - // Arrange - var options = new DeviceExportOptions { ParamSetWhitelist = ["MASTER", "VALUES"] }; - - // Act - var result = options.IsParamSetAllowed("MASTER"); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void IsParamSetAllowed_WithNonMatchingKey_ReturnsFalse() - { - // Arrange - var options = new DeviceExportOptions { ParamSetWhitelist = ["MASTER", "VALUES"] }; - - // Act - var result = options.IsParamSetAllowed("LINK"); - - // Assert - result.Should().BeFalse(); - } + { null, "MASTER", true }, + { [], "MASTER", true }, + { null, string.Empty, true }, + { [], string.Empty, true }, + { ["MASTER", "VALUES"], "MASTER", true }, + { ["MASTER", "VALUES"], "VALUES", true }, + { ["MASTER", "VALUES"], "LINK", false }, + { ["MASTER"], string.Empty, false }, + { ["MASTER"], "master", true }, + { ["MASTER"], "Master", true }, + { ["master"], "MASTER", true } + }; [Theory] - [InlineData("master")] - [InlineData("Master")] - [InlineData("MASTER")] - public void IsParamSetAllowed_WithDifferentCasing_ReturnsTrue(string key) + [MemberData(nameof(ParamSetAllowedCases))] + public void IsParamSetAllowed_WithWhitelistAndKey_ReturnsExpected(string[]? whitelist, string key, bool expected) { // Arrange - var options = new DeviceExportOptions { ParamSetWhitelist = ["MASTER"] }; + var options = new DeviceExportOptions { ParamSetWhitelist = whitelist }; // Act var result = options.IsParamSetAllowed(key); // Assert - result.Should().BeTrue(); - } - - [Fact] - public void IsParamSetAllowed_WithEmptyStringKey_ReturnsFalseWhenNotInWhitelist() - { - // Arrange - var options = new DeviceExportOptions { ParamSetWhitelist = ["MASTER"] }; - - // Act - var result = options.IsParamSetAllowed(string.Empty); - - // Assert - result.Should().BeFalse(); - } - - [Fact] - public void IsParamSetAllowed_WithEmptyStringKeyAndEmptyWhitelist_ReturnsTrue() - { - // Arrange - var options = new DeviceExportOptions { ParamSetWhitelist = [] }; - - // Act - var result = options.IsParamSetAllowed(string.Empty); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void IsParamValueNameAllowed_WithNullWhitelist_ReturnsTrue() - { - // Arrange - var options = new DeviceExportOptions { ParamValueNameWhitelist = null }; - - // Act - var result = options.IsParamValueNameAllowed("BOOST_TIME"); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void IsParamValueNameAllowed_WithEmptyWhitelist_ReturnsTrue() - { - // Arrange - var options = new DeviceExportOptions { ParamValueNameWhitelist = [] }; - - // Act - var result = options.IsParamValueNameAllowed("BOOST_TIME"); - - // Assert - result.Should().BeTrue(); + result.Should().Be(expected); } - [Fact] - public void IsParamValueNameAllowed_WithMatchingName_ReturnsTrue() + public static TheoryData ParamValueNameAllowedCases => new() { - // Arrange - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME", "SET_TEMPERATURE"] }; - - // Act - var result = options.IsParamValueNameAllowed("BOOST_TIME"); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void IsParamValueNameAllowed_WithNonMatchingName_ReturnsFalse() - { - // Arrange - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME", "SET_TEMPERATURE"] }; - - // Act - var result = options.IsParamValueNameAllowed("ACTUAL_TEMPERATURE"); - - // Assert - result.Should().BeFalse(); - } + { null, "BOOST_TIME", true }, + { [], "BOOST_TIME", true }, + { null, string.Empty, true }, + { [], string.Empty, true }, + { ["BOOST_TIME", "SET_TEMPERATURE"], "BOOST_TIME", true }, + { ["BOOST_TIME", "SET_TEMPERATURE"], "SET_TEMPERATURE", true }, + { ["BOOST_TIME", "SET_TEMPERATURE"], "ACTUAL_TEMPERATURE", false }, + { ["BOOST_TIME"], string.Empty, false }, + { ["BOOST_TIME"], "boost_time", true }, + { ["BOOST_TIME"], "Boost_Time", true }, + { ["boost_time"], "BOOST_TIME", true } + }; [Theory] - [InlineData("boost_time")] - [InlineData("Boost_Time")] - [InlineData("BOOST_TIME")] - public void IsParamValueNameAllowed_WithDifferentCasing_ReturnsTrue(string name) + [MemberData(nameof(ParamValueNameAllowedCases))] + public void IsParamValueNameAllowed_WithWhitelistAndName_ReturnsExpected(string[]? whitelist, string name, bool expected) { // Arrange - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME"] }; + var options = new DeviceExportOptions { ParamValueNameWhitelist = whitelist }; // Act var result = options.IsParamValueNameAllowed(name); // Assert - result.Should().BeTrue(); + result.Should().Be(expected); } [Fact] - public void IsParamValueNameAllowed_WithEmptyStringName_ReturnsFalseWhenNotInWhitelist() + public void WriteIndented_DefaultValue_IsTrue() { - // Arrange - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME"] }; - - // Act - var result = options.IsParamValueNameAllowed(string.Empty); + // Arrange & Act + var options = new DeviceExportOptions(); // Assert - result.Should().BeFalse(); + options.WriteIndented.Should().BeTrue(); } [Fact] - public void IsParamValueNameAllowed_WithEmptyStringNameAndEmptyWhitelist_ReturnsTrue() + public void ParamSetWhitelist_DefaultValue_IsNull() { - // Arrange - var options = new DeviceExportOptions { ParamValueNameWhitelist = [] }; - - // Act - var result = options.IsParamValueNameAllowed(string.Empty); + // Arrange & Act + var options = new DeviceExportOptions(); // Assert - result.Should().BeTrue(); + options.ParamSetWhitelist.Should().BeNull(); } [Fact] - public void WriteIndented_DefaultValue_IsTrue() + public void ParamValueNameWhitelist_DefaultValue_IsNull() { // Arrange & Act var options = new DeviceExportOptions(); // Assert - options.WriteIndented.Should().BeTrue(); + options.ParamValueNameWhitelist.Should().BeNull(); } } diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs index 4649c69..ea8111a 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/DeviceExporterTests.cs @@ -1,121 +1,25 @@ using System.Text.Json; -using CreativeCoders.HomeMatic.Core; -using CreativeCoders.HomeMatic.Core.Devices; -using CreativeCoders.HomeMatic.Exporting; -using CreativeCoders.HomeMatic.XmlRpc; -using CreativeCoders.HomeMatic.XmlRpc.Parameters; -using FakeItEasy; using AwesomeAssertions; +using CreativeCoders.HomeMatic.Exporting; namespace CreativeCoders.HomeMatic.Tests.Exporting; public class DeviceExporterTests { - private static CcuDeviceUri CreateDeviceUri(string host = "ccu2.local", string address = "ABC123456") - { - return new CcuDeviceUri - { - CcuHost = host, - Kind = CcuDeviceKind.HomeMatic, - Address = address - }; - } - - /// - /// Parameter object for configuring a fake in tests. - /// - private sealed class FakeDeviceOptions - { - public string Name { get; init; } = "TestDevice"; - public string Address { get; init; } = "ABC123456"; - public string DeviceType { get; init; } = "HM-CC-RT-DN"; - public string Firmware { get; init; } = "1.4"; - public string CcuHost { get; init; } = "ccu2.local"; - public string[]? ParamSetKeys { get; init; } - public IEnumerable? ParamSetValues { get; init; } - public IEnumerable? Channels { get; init; } - } - - private static ICompleteCcuDevice CreateFakeDevice(FakeDeviceOptions? options = null) - { - var opts = options ?? new FakeDeviceOptions(); - var device = A.Fake(); - var deviceData = A.Fake(); - var uri = CreateDeviceUri(opts.CcuHost, opts.Address); - - A.CallTo(() => deviceData.Name).Returns(opts.Name); - A.CallTo(() => deviceData.Uri).Returns(uri); - A.CallTo(() => deviceData.DeviceType).Returns(opts.DeviceType); - A.CallTo(() => deviceData.Firmware).Returns(opts.Firmware); - A.CallTo(() => deviceData.ParamSets).Returns(opts.ParamSetKeys ?? ["MASTER", "VALUES"]); - - A.CallTo(() => device.DeviceData).Returns(deviceData); - A.CallTo(() => device.ParamSetValues).Returns(opts.ParamSetValues ?? []); - A.CallTo(() => device.Channels).Returns(opts.Channels ?? []); - - return device; - } - - private static ICompleteCcuDeviceChannel CreateFakeChannel( - string address = "ABC123456:1", - string deviceType = "HM-CC-RT-DN:01", - int index = 1, - string[]? paramSets = null, - IEnumerable? paramSetValues = null) - { - var channel = A.Fake(); - var channelData = A.Fake(); - var uri = new CcuDeviceUri - { - CcuHost = "ccu2.local", - Kind = CcuDeviceKind.HomeMatic, - Address = address - }; - - A.CallTo(() => channelData.Uri).Returns(uri); - A.CallTo(() => channelData.DeviceType).Returns(deviceType); - A.CallTo(() => channelData.Index).Returns(index); - A.CallTo(() => channelData.ParamSets).Returns(paramSets ?? ["VALUES"]); - - A.CallTo(() => channel.ChannelData).Returns(channelData); - A.CallTo(() => channel.ParamSetValues).Returns(paramSetValues ?? []); - - return channel; - } - - private static ParamSetValuesWithDescriptions CreateParamSetValues( - string paramSetKey, - params (string name, object value, string descriptionId)[] values) - { - return new ParamSetValuesWithDescriptions - { - ParamSetKey = paramSetKey, - ParamSetValues = values.Select(v => new ParamSetValueWithDescription - { - ParamSetValue = new ParamSetValue { Name = v.name, Value = v.value }, - Description = new CcuParameterDescription - { - Id = v.descriptionId, - DefaultValue = null, - MinValue = null, - MaxValue = null, - Type = null, - DataType = ParameterDataType.Float, - Unit = null, - TabOrder = 0, - Control = null, - ValuesList = [], - SpecialValues = [] - } - }).ToList() - }; - } + // ---- Mapping: BuildExportData ----------------------------------------- [Fact] - public void BuildExportData_WithValidDevice_MapsNameCorrectly() + public void BuildExportData_WithFullyPopulatedDevice_MapsAllScalarProperties() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Name = "MyDevice" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithName("MyDevice") + .WithAddress("DEF789") + .WithDeviceType("HM-ES-PMSw1-Pl") + .WithFirmware("2.7.1") + .WithCcuHost("192.168.1.100") + .WithParamSetKeys("MASTER", "VALUES", "LINK") + .Build(); var sut = new DeviceExporter(); // Act @@ -123,702 +27,680 @@ public void BuildExportData_WithValidDevice_MapsNameCorrectly() // Assert result.Name.Should().Be("MyDevice"); + result.Address.Should().Be("DEF789"); + result.DeviceType.Should().Be("HM-ES-PMSw1-Pl"); + result.FirmwareVersion.Should().Be("2.7.1"); + result.Ccu.Should().Be("192.168.1.100"); + result.ParamSetKeys.Should().BeEquivalentTo("MASTER", "VALUES", "LINK"); } [Fact] - public void BuildExportData_WithValidDevice_MapsAddressCorrectly() + public void BuildExportData_WithCcuNameSet_UsesCcuNameForCcu() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Address = "DEF789" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithCcuHost("192.168.1.100") + .WithCcuName("MyCCU2") + .Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.Address.Should().Be("DEF789"); + result.Ccu.Should().Be("MyCCU2"); } [Fact] - public void BuildExportData_WithValidDevice_MapsDeviceTypeCorrectly() + public void BuildExportData_WithCcuNameEmpty_FallsBackToCcuHost() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { DeviceType = "HM-ES-PMSw1-Pl" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithCcuHost("ccu2.local") + .WithCcuName(string.Empty) + .Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.DeviceType.Should().Be("HM-ES-PMSw1-Pl"); + result.Ccu.Should().Be("ccu2.local"); } [Fact] - public void BuildExportData_WithValidDevice_MapsFirmwareVersionCorrectly() + public void BuildExportData_WithNoChannels_ReturnsEmptyChannelsList() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Firmware = "2.7.1" }); + var device = new CompleteCcuDeviceFakeBuilder().Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.FirmwareVersion.Should().Be("2.7.1"); + result.Channels.Should().BeEmpty(); } [Fact] - public void BuildExportData_WithCcuHostName_UsesCcuNameAsHostDisplayName() + public void BuildExportData_WithMultipleChannels_MapsAllInOrder() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { CcuHost = "192.168.1.100" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithChannel(c => c.WithAddress("XYZ:1").WithIndex(1)) + .WithChannel(c => c.WithAddress("XYZ:2").WithIndex(2)) + .WithChannel(c => c.WithAddress("XYZ:3").WithIndex(3)) + .Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.Ccu.Should().Be("192.168.1.100"); + result.Channels.Should().HaveCount(3); + result.Channels.Select(c => c.Index).Should().ContainInOrder(1, 2, 3); + result.Channels.Select(c => c.Address).Should().ContainInOrder("XYZ:1", "XYZ:2", "XYZ:3"); } [Fact] - public void BuildExportData_WithParamSetKeys_MapsParamSetKeysCorrectly() + public void BuildExportData_WithChannel_MapsChannelScalarProperties() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetKeys = ["MASTER", "VALUES", "LINK"] }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithChannel(c => c + .WithAddress("XYZ:1") + .WithDeviceType("HM-CC-RT-DN:01") + .WithIndex(1) + .WithParamSets("VALUES", "MASTER")) + .Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.ParamSetKeys.Should().BeEquivalentTo("MASTER", "VALUES", "LINK"); + var channel = result.Channels.Single(); + channel.Address.Should().Be("XYZ:1"); + channel.DeviceType.Should().Be("HM-CC-RT-DN:01"); + channel.Index.Should().Be(1); + channel.ParamSets.Should().BeEquivalentTo("VALUES", "MASTER"); } [Fact] - public void BuildExportData_WithNoChannels_ReturnsEmptyChannelsList() + public void BuildExportData_WithNoParamSetValues_ReturnsEmptyList() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [] }); + var device = new CompleteCcuDeviceFakeBuilder().Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.Channels.Should().BeEmpty(); + result.ParamSetValues.Should().BeEmpty(); } [Fact] - public void BuildExportData_WithChannels_MapsChannelsCorrectly() + public void BuildExportData_WithParamSetValues_MapsKeyAndValues() { // Arrange - var channel = CreateFakeChannel(address: "XYZ:1", deviceType: "HM-CC-RT-DN:01", index: 1); - var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [channel] }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p.Add("BOOST_TIME", 5, "Boost Time")) + .Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.Channels.Should().HaveCount(1); - var channelResult = result.Channels.First(); - channelResult.Address.Should().Be("XYZ:1"); - channelResult.DeviceType.Should().Be("HM-CC-RT-DN:01"); - channelResult.Index.Should().Be(1); + var paramSet = result.ParamSetValues.Single(); + paramSet.ParamSetKey.Should().Be("MASTER"); + + var value = paramSet.Values.Single(); + value.Key.Should().Be("BOOST_TIME"); + value.Value.Should().Be(5); + value.Name.Should().Be("Boost Time"); } [Fact] - public void BuildExportData_WithParamSetValues_MapsParamSetValuesCorrectly() + public void BuildExportData_WithEmptyParamSet_ReturnsParamSetWithEmptyValues() { // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("MASTER", ("BOOST_TIME", 5, "Boost Time")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER") + .Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - result.ParamSetValues.Should().HaveCount(1); - var paramSet = result.ParamSetValues.First(); + var paramSet = result.ParamSetValues.Single(); paramSet.ParamSetKey.Should().Be("MASTER"); - paramSet.Values.Should().HaveCount(1); - paramSet.Values.First().Key.Should().Be("BOOST_TIME"); - paramSet.Values.First().Value.Should().Be(5); - paramSet.Values.First().Name.Should().Be("Boost Time"); + paramSet.Values.Should().BeEmpty(); } [Fact] - public void BuildExportData_WithOptionsWhitelistingParamSet_FiltersOutNonWhitelistedParamSets() + public void BuildExportData_WithDescriptionIdEqualToName_SetsNameToNull() { - // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("MASTER", ("BOOST_TIME", 5, "Boost Time")), - CreateParamSetValues("LINK", ("PEER_NEEDS_BURST", true, "Peer Needs Burst")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamSetWhitelist = ["MASTER"] }; + // Arrange – same name used for key and description id → dedup to null + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("VALUES", p => p.Add("ACTIVE", true, "ACTIVE")) + .Build(); var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var result = sut.BuildExportData(device); // Assert - result.ParamSetValues.Should().HaveCount(1); - result.ParamSetValues.First().ParamSetKey.Should().Be("MASTER"); + result.ParamSetValues.Single().Values.Single().Name.Should().BeNull(); } [Fact] - public void BuildExportData_WithNullOptions_IncludesAllParamSets() + public void BuildExportData_WithDescriptionIdDifferentFromName_SetsNameToDescriptionId() { // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("MASTER", ("BOOST_TIME", 5, "Boost Time")), - CreateParamSetValues("VALUES", ("SET_TEMPERATURE", 21.0, "Set Temperature")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("VALUES", p => p.Add("BOOST_TIME", 5, "Boost Time")) + .Build(); var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, null); + var result = sut.BuildExportData(device); // Assert - result.ParamSetValues.Should().HaveCount(2); + result.ParamSetValues.Single().Values.Single().Name.Should().Be("Boost Time"); } [Fact] - public void BuildExportData_WithChannelParamSetValues_MapsChannelParamSetValuesCorrectly() + public void BuildExportData_WithDescriptionIdNull_SetsNameToNull() { - // Arrange - var channelParamSetValues = new[] - { - CreateParamSetValues("VALUES", ("SET_TEMPERATURE", 21.5, "Set Temperature")) - }; - var channel = CreateFakeChannel(paramSetValues: channelParamSetValues); - var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [channel] }); + // Arrange – descriptionId null → Description.Id is null, Name is null + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("VALUES", p => p.Add("ACTIVE", true, descriptionId: null)) + .Build(); var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device); // Assert - var channelResult = result.Channels.First(); - channelResult.ParamSetValues.Should().HaveCount(1); - channelResult.ParamSetValues.First().ParamSetKey.Should().Be("VALUES"); - channelResult.ParamSetValues.First().Values.First().Key.Should().Be("SET_TEMPERATURE"); + result.ParamSetValues.Single().Values.Single().Name.Should().BeNull(); } [Fact] - public async Task ExportDeviceAsync_WithValidDevice_ReturnsValidJson() + public void BuildExportData_WithChannelParamSetValues_MapsChannelParamSetValues() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Name = "TestDevice" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithChannel(c => c.WithParamSet("VALUES", + p => p.Add("SET_TEMPERATURE", 21.5, "Set Temperature"))) + .Build(); var sut = new DeviceExporter(); // Act - var json = await sut.ExportDeviceAsync(device); + var result = sut.BuildExportData(device); // Assert - var act = () => JsonDocument.Parse(json); - act.Should().NotThrow(); + var channel = result.Channels.Single(); + var paramSet = channel.ParamSetValues.Single(); + paramSet.ParamSetKey.Should().Be("VALUES"); + paramSet.Values.Single().Key.Should().Be("SET_TEMPERATURE"); } + // ---- Filter: BuildExportData + Options -------------------------------- + [Fact] - public async Task ExportDeviceAsync_WithValidDevice_ContainsDeviceName() + public void BuildExportData_WithNullOptions_IncludesAllParamSets() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Name = "MyThermostat" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p.Add("BOOST_TIME", 5, "Boost Time")) + .WithParamSet("VALUES", p => p.Add("SET_TEMPERATURE", 21.0, "Set Temperature")) + .Build(); var sut = new DeviceExporter(); // Act - var json = await sut.ExportDeviceAsync(device); + var result = sut.BuildExportData(device, null); // Assert - json.Should().Contain("MyThermostat"); + result.ParamSetValues.Select(p => p.ParamSetKey) + .Should().BeEquivalentTo("MASTER", "VALUES"); } [Fact] - public async Task ExportDeviceAsync_WithWriteIndentedTrue_ReturnsIndentedJson() + public void BuildExportData_WithParamSetWhitelist_FiltersDeviceParamSets() { // Arrange - var device = CreateFakeDevice(); - var options = new DeviceExportOptions { WriteIndented = true }; + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p.Add("BOOST_TIME", 5, "Boost Time")) + .WithParamSet("LINK", p => p.Add("PEER_NEEDS_BURST", true, "Peer Needs Burst")) + .Build(); + var options = new DeviceExportOptions { ParamSetWhitelist = ["MASTER"] }; var sut = new DeviceExporter(); // Act - var json = await sut.ExportDeviceAsync(device, options); + var result = sut.BuildExportData(device, options); // Assert - json.Should().Contain("\n"); + result.ParamSetValues.Select(p => p.ParamSetKey).Should().BeEquivalentTo("MASTER"); } [Fact] - public async Task ExportDeviceAsync_WithWriteIndentedFalse_ReturnsCompactJson() + public void BuildExportData_WithParamSetWhitelist_FiltersChannelParamSets() { // Arrange - var device = CreateFakeDevice(); - var options = new DeviceExportOptions { WriteIndented = false }; + var device = new CompleteCcuDeviceFakeBuilder() + .WithChannel(c => c + .WithParamSet("VALUES", p => p.Add("SET_TEMPERATURE", 21.5, "Set Temperature")) + .WithParamSet("LINK", p => p.Add("PEER_NEEDS_BURST", true, "Peer Needs Burst"))) + .Build(); + var options = new DeviceExportOptions { ParamSetWhitelist = ["VALUES"] }; var sut = new DeviceExporter(); // Act - var json = await sut.ExportDeviceAsync(device, options); + var result = sut.BuildExportData(device, options); // Assert - json.Should().NotContain("\n"); + result.Channels.Single().ParamSetValues.Select(p => p.ParamSetKey) + .Should().BeEquivalentTo("VALUES"); } [Fact] - public async Task ExportDevicesAsync_WithMultipleDevices_ReturnsJsonArray() + public void BuildExportData_WithParamValueNameWhitelist_FiltersDeviceValues() { // Arrange - var device1 = CreateFakeDevice(new FakeDeviceOptions { Name = "Device1", Address = "ADDR1" }); - var device2 = CreateFakeDevice(new FakeDeviceOptions { Name = "Device2", Address = "ADDR2" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p + .Add("BOOST_TIME", 5, "Boost Time") + .Add("DECALCIFICATION_TIME", 22, "Decalcification Time") + .Add("PARTY_MODE", false, "Party Mode")) + .Build(); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME", "PARTY_MODE"] }; var sut = new DeviceExporter(); // Act - var json = await sut.ExportDevicesAsync([device1, device2]); + var result = sut.BuildExportData(device, options); // Assert - var document = JsonDocument.Parse(json); - document.RootElement.ValueKind.Should().Be(JsonValueKind.Array); - document.RootElement.GetArrayLength().Should().Be(2); + result.ParamSetValues.Single().Values.Select(v => v.Key) + .Should().BeEquivalentTo("BOOST_TIME", "PARTY_MODE"); } [Fact] - public async Task ExportDevicesAsync_WithEmptyList_ReturnsEmptyJsonArray() + public void BuildExportData_WithParamValueNameWhitelist_FiltersChannelValues() { // Arrange + var device = new CompleteCcuDeviceFakeBuilder() + .WithChannel(c => c.WithParamSet("VALUES", p => p + .Add("SET_TEMPERATURE", 21.5, "Set Temperature") + .Add("ACTUAL_TEMPERATURE", 20.1, "Actual Temperature") + .Add("VALVE_STATE", 45, "Valve State"))) + .Build(); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["SET_TEMPERATURE", "VALVE_STATE"] }; var sut = new DeviceExporter(); // Act - var json = await sut.ExportDevicesAsync([]); + var result = sut.BuildExportData(device, options); // Assert - var document = JsonDocument.Parse(json); - document.RootElement.ValueKind.Should().Be(JsonValueKind.Array); - document.RootElement.GetArrayLength().Should().Be(0); + result.Channels.Single().ParamSetValues.Single().Values.Select(v => v.Key) + .Should().BeEquivalentTo("SET_TEMPERATURE", "VALVE_STATE"); } [Fact] - public async Task ExportDevicesAsync_WithOptions_FiltersParamSetsPerDevice() + public void BuildExportData_WithBothWhitelists_AppliesBothFilters() { // Arrange - var paramSetValues = new[] + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p + .Add("BOOST_TIME", 5, "Boost Time") + .Add("DECALCIFICATION_TIME", 22, "Decalcification Time")) + .WithParamSet("LINK", p => p.Add("PEER_NEEDS_BURST", true, "Peer Needs Burst")) + .Build(); + var options = new DeviceExportOptions { - CreateParamSetValues("MASTER", ("BOOST_TIME", 5, "Boost Time")), - CreateParamSetValues("LINK", ("PEER_NEEDS_BURST", true, "Peer Needs Burst")) + ParamSetWhitelist = ["MASTER"], + ParamValueNameWhitelist = ["BOOST_TIME"] }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamSetWhitelist = ["MASTER"] }; var sut = new DeviceExporter(); // Act - var json = await sut.ExportDevicesAsync([device], options); + var result = sut.BuildExportData(device, options); // Assert - json.Should().Contain("MASTER"); - json.Should().NotContain("LINK"); + var paramSet = result.ParamSetValues.Single(); + paramSet.ParamSetKey.Should().Be("MASTER"); + paramSet.Values.Select(v => v.Key).Should().BeEquivalentTo("BOOST_TIME"); } - [Fact] - public async Task ExportDeviceAsync_WithCamelCasePolicy_UsesCamelCasePropertyNames() + [Theory] + [InlineData("master")] + [InlineData("Master")] + [InlineData("MASTER")] + public void BuildExportData_WithParamSetWhitelistCaseVariants_MatchesCaseInsensitively(string whitelistKey) { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Name = "TestDevice" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p.Add("BOOST_TIME", 5, "Boost Time")) + .Build(); + var options = new DeviceExportOptions { ParamSetWhitelist = [whitelistKey] }; var sut = new DeviceExporter(); // Act - var json = await sut.ExportDeviceAsync(device); + var result = sut.BuildExportData(device, options); // Assert - json.Should().Contain("\"name\""); - json.Should().Contain("\"address\""); - json.Should().Contain("\"deviceType\""); - json.Should().NotContain("\"Name\""); - json.Should().NotContain("\"Address\""); + result.ParamSetValues.Select(p => p.ParamSetKey).Should().BeEquivalentTo("MASTER"); } - [Fact] - public void BuildExportData_WithMultipleChannels_MapsAllChannels() + [Theory] + [InlineData("boost_time")] + [InlineData("Boost_Time")] + [InlineData("BOOST_TIME")] + public void BuildExportData_WithParamValueNameWhitelistCaseVariants_MatchesCaseInsensitively(string whitelistName) { // Arrange - var channel1 = CreateFakeChannel(address: "XYZ:1", index: 1); - var channel2 = CreateFakeChannel(address: "XYZ:2", index: 2); - var channel3 = CreateFakeChannel(address: "XYZ:3", index: 3); - var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [channel1, channel2, channel3] }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p + .Add("BOOST_TIME", 5, "Boost Time") + .Add("DECALCIFICATION_TIME", 22, "Decalcification Time")) + .Build(); + var options = new DeviceExportOptions { ParamValueNameWhitelist = [whitelistName] }; var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device); + var result = sut.BuildExportData(device, options); // Assert - result.Channels.Should().HaveCount(3); - result.Channels.Select(c => c.Index).Should().BeEquivalentTo([1, 2, 3]); + result.ParamSetValues.Single().Values.Select(v => v.Key) + .Should().BeEquivalentTo("BOOST_TIME"); } [Fact] - public void BuildExportData_WithChannelOptionsWhitelist_FiltersChannelParamSets() + public void BuildExportData_WithParamValueNameWhitelistFilteringAllValues_ReturnsParamSetWithEmptyValues() { - // Arrange - var channelParamSetValues = new[] - { - CreateParamSetValues("VALUES", ("SET_TEMPERATURE", 21.5, "Set Temperature")), - CreateParamSetValues("LINK", ("PEER_NEEDS_BURST", true, "Peer Needs Burst")) - }; - var channel = CreateFakeChannel(paramSetValues: channelParamSetValues); - var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [channel] }); - var options = new DeviceExportOptions { ParamSetWhitelist = ["VALUES"] }; + // Arrange – paramset itself stays, but all its values are filtered out + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p + .Add("BOOST_TIME", 5, "Boost Time") + .Add("DECALCIFICATION_TIME", 22, "Decalcification Time")) + .Build(); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["NONEXISTENT"] }; var sut = new DeviceExporter(); // Act var result = sut.BuildExportData(device, options); // Assert - result.Channels.First().ParamSetValues.Should().HaveCount(1); - result.Channels.First().ParamSetValues.First().ParamSetKey.Should().Be("VALUES"); + var paramSet = result.ParamSetValues.Single(); + paramSet.ParamSetKey.Should().Be("MASTER"); + paramSet.Values.Should().BeEmpty(); } - [Fact] - public void BuildExportData_WithChannelOptionsWhitelistCaseInsensitive_FiltersCorrectly() + [Theory] + [InlineData(42)] + [InlineData(3.14)] + [InlineData(true)] + [InlineData("on")] + public void BuildExportData_WithVariousValueTypes_PreservesValue(object value) { // Arrange - var channelParamSetValues = new[] - { - CreateParamSetValues("VALUES", ("SET_TEMPERATURE", 21.5, "Set Temperature")), - CreateParamSetValues("LINK", ("PEER_NEEDS_BURST", true, "Peer Needs Burst")) - }; - var channel = CreateFakeChannel(paramSetValues: channelParamSetValues); - var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [channel] }); - // Use lowercase key in whitelist to test case-insensitive matching - var options = new DeviceExportOptions { ParamSetWhitelist = ["values"] }; + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("VALUES", p => p.Add("VAL", value, "Val")) + .Build(); var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var result = sut.BuildExportData(device); // Assert - result.Channels.First().ParamSetValues.Should().HaveCount(1); - result.Channels.First().ParamSetValues.First().ParamSetKey.Should().Be("VALUES"); + result.ParamSetValues.Single().Values.Single().Value.Should().Be(value); } [Fact] - public void BuildExportData_WithEmptyParamSetValues_ReturnsEmptyParamSetValuesList() + public void BuildExportData_WithParamValueNameWhitelistNonMatching_ReturnsEmptyValues() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = [] }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("VALUES", p => p.Add("SET_TEMPERATURE", 21.5, "Set Temperature")) + .Build(); + var options = new DeviceExportOptions { ParamValueNameWhitelist = ["NONEXISTENT"] }; var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device); + var result = sut.BuildExportData(device, options); // Assert - result.ParamSetValues.Should().BeEmpty(); + result.ParamSetValues.Single().Values.Should().BeEmpty(); } + // ---- ExportDeviceAsync / JSON ----------------------------------------- + [Fact] - public async Task ExportDeviceAsync_WithNullParamValueName_OmitsNamePropertyFromJson() + public async Task ExportDeviceAsync_WithValidDevice_ReturnsValidJson() { // Arrange - var paramSetValues = new[] - { - new ParamSetValuesWithDescriptions - { - ParamSetKey = "VALUES", - ParamSetValues = - [ - new ParamSetValueWithDescription - { - ParamSetValue = new ParamSetValue { Name = "ACTIVE", Value = true }, - Description = new CcuParameterDescription - { - Id = null, // null description id → Name will be null - DefaultValue = null, - MinValue = null, - MaxValue = null, - Type = null, - DataType = ParameterDataType.Bool, - Unit = null, - TabOrder = 0, - Control = null, - ValuesList = [], - SpecialValues = [] - } - } - ] - } - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var device = new CompleteCcuDeviceFakeBuilder().Build(); var sut = new DeviceExporter(); // Act var json = await sut.ExportDeviceAsync(device); // Assert - // DefaultIgnoreCondition.WhenWritingNull should omit null name - json.Should().NotContain("\"name\":null"); + var act = () => JsonDocument.Parse(json); + act.Should().NotThrow(); } [Fact] - public async Task ExportDevicesAsync_WithSingleDevice_ReturnsArrayWithOneElement() + public async Task ExportDeviceAsync_WithDefaultOptions_UsesCamelCasePropertyNames() { // Arrange - var device = CreateFakeDevice(new FakeDeviceOptions { Name = "OnlyDevice" }); + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p.Add("BOOST_TIME", 5, "Boost Time")) + .WithChannel() + .Build(); var sut = new DeviceExporter(); // Act - var json = await sut.ExportDevicesAsync([device]); + var json = await sut.ExportDeviceAsync(device); // Assert - var document = JsonDocument.Parse(json); - document.RootElement.ValueKind.Should().Be(JsonValueKind.Array); - document.RootElement.GetArrayLength().Should().Be(1); + using var document = JsonDocument.Parse(json); + var root = document.RootElement; + root.TryGetProperty("name", out _).Should().BeTrue(); + root.TryGetProperty("address", out _).Should().BeTrue(); + root.TryGetProperty("deviceType", out _).Should().BeTrue(); + root.TryGetProperty("firmwareVersion", out _).Should().BeTrue(); + root.TryGetProperty("ccu", out _).Should().BeTrue(); + root.TryGetProperty("paramSetKeys", out _).Should().BeTrue(); + root.TryGetProperty("paramSetValues", out _).Should().BeTrue(); + root.TryGetProperty("channels", out _).Should().BeTrue(); } [Fact] - public async Task ExportDeviceAsync_WithCcuNameSet_UsesCcuNameInOutput() + public async Task ExportDeviceAsync_WithWriteIndentedTrue_ReturnsIndentedJson() { // Arrange - var device = A.Fake(); - var deviceData = A.Fake(); - var uri = new CcuDeviceUri - { - CcuHost = "192.168.1.100", - CcuName = "MyCCU2", - Kind = CcuDeviceKind.HomeMatic, - Address = "ADDR1" - }; - A.CallTo(() => deviceData.Name).Returns("Device"); - A.CallTo(() => deviceData.Uri).Returns(uri); - A.CallTo(() => deviceData.DeviceType).Returns("HM-CC-RT-DN"); - A.CallTo(() => deviceData.Firmware).Returns("1.0"); - A.CallTo(() => deviceData.ParamSets).Returns([]); - A.CallTo(() => device.DeviceData).Returns(deviceData); - A.CallTo(() => device.ParamSetValues).Returns([]); - A.CallTo(() => device.Channels).Returns([]); + var device = new CompleteCcuDeviceFakeBuilder().Build(); + var options = new DeviceExportOptions { WriteIndented = true }; var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device); + var json = await sut.ExportDeviceAsync(device, options); // Assert - // HostDisplayName returns CcuName when set - result.Ccu.Should().Be("MyCCU2"); + json.Should().Contain("\n"); } [Fact] - public void BuildExportData_WithEmptyParamSetInParamSetValues_ReturnsParamSetWithEmptyValues() + public async Task ExportDeviceAsync_WithWriteIndentedFalse_ReturnsCompactJson() { // Arrange - var paramSetValues = new[] - { - new ParamSetValuesWithDescriptions - { - ParamSetKey = "MASTER", - ParamSetValues = [] - } - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); + var device = new CompleteCcuDeviceFakeBuilder().Build(); + var options = new DeviceExportOptions { WriteIndented = false }; var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device); + var json = await sut.ExportDeviceAsync(device, options); // Assert - result.ParamSetValues.Should().HaveCount(1); - result.ParamSetValues.First().Values.Should().BeEmpty(); + json.Should().NotContain("\n"); } [Fact] - public void BuildExportData_WithParamValueNameWhitelist_FiltersOutNonWhitelistedValues() + public async Task ExportDeviceAsync_WithNullOptions_ReturnsIndentedJson() { - // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("MASTER", - ("BOOST_TIME", 5, "Boost Time"), - ("DECALCIFICATION_TIME", 22, "Decalcification Time"), - ("PARTY_MODE", false, "Party Mode")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME", "PARTY_MODE"] }; + // Arrange – default behavior when no options are provided is indented + var device = new CompleteCcuDeviceFakeBuilder().Build(); var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var json = await sut.ExportDeviceAsync(device); // Assert - result.ParamSetValues.Should().HaveCount(1); - var values = result.ParamSetValues.First().Values.ToList(); - values.Should().HaveCount(2); - values.Select(v => v.Key).Should().BeEquivalentTo("BOOST_TIME", "PARTY_MODE"); + json.Should().Contain("\n"); } [Fact] - public void BuildExportData_WithParamValueNameWhitelistNonMatching_ReturnsEmptyValues() + public async Task ExportDeviceAsync_WithNullParamValueName_OmitsNamePropertyFromJson() { - // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("VALUES", ("SET_TEMPERATURE", 21.5, "Set Temperature")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["NONEXISTENT"] }; + // Arrange – descriptionId == name triggers Name=null, which should be omitted + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("VALUES", p => p.Add("ACTIVE", true, "ACTIVE")) + .Build(); + var options = new DeviceExportOptions { WriteIndented = false }; var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var json = await sut.ExportDeviceAsync(device, options); // Assert - result.ParamSetValues.Should().HaveCount(1); - result.ParamSetValues.First().Values.Should().BeEmpty(); + json.Should().NotContain("\"name\":null"); } [Fact] - public void BuildExportData_WithNullParamValueNameWhitelist_IncludesAllValues() + public async Task ExportDeviceAsync_WithPopulatedDevice_RoundTripsToEquivalentStructure() { // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("MASTER", - ("BOOST_TIME", 5, "Boost Time"), - ("DECALCIFICATION_TIME", 22, "Decalcification Time")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamValueNameWhitelist = null }; + var device = new CompleteCcuDeviceFakeBuilder() + .WithName("RoundTripDevice") + .WithAddress("RT001") + .WithDeviceType("HM-CC-RT-DN") + .WithFirmware("1.4") + .WithParamSetKeys("MASTER", "VALUES") + .WithParamSet("MASTER", p => p.Add("BOOST_TIME", 5, "Boost Time")) + .WithChannel(c => c + .WithAddress("RT001:1") + .WithIndex(1) + .WithParamSet("VALUES", p => p.Add("SET_TEMPERATURE", 21.5, "Set Temperature"))) + .Build(); var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var json = await sut.ExportDeviceAsync(device); + var deserialized = JsonSerializer.Deserialize( + json, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); // Assert - result.ParamSetValues.First().Values.Should().HaveCount(2); - } + deserialized.Should().NotBeNull(); + deserialized!.Name.Should().Be("RoundTripDevice"); + deserialized.Address.Should().Be("RT001"); + deserialized.DeviceType.Should().Be("HM-CC-RT-DN"); + deserialized.FirmwareVersion.Should().Be("1.4"); + deserialized.ParamSetKeys.Should().BeEquivalentTo("MASTER", "VALUES"); - [Fact] - public void BuildExportData_WithEmptyParamValueNameWhitelist_IncludesAllValues() - { - // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("MASTER", - ("BOOST_TIME", 5, "Boost Time"), - ("DECALCIFICATION_TIME", 22, "Decalcification Time")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamValueNameWhitelist = [] }; - var sut = new DeviceExporter(); - - // Act - var result = sut.BuildExportData(device, options); + var paramSet = deserialized.ParamSetValues.Single(); + paramSet.ParamSetKey.Should().Be("MASTER"); + paramSet.Values.Single().Key.Should().Be("BOOST_TIME"); - // Assert - result.ParamSetValues.First().Values.Should().HaveCount(2); + var channel = deserialized.Channels.Single(); + channel.Address.Should().Be("RT001:1"); + channel.Index.Should().Be(1); + channel.ParamSetValues.Single().Values.Single().Key.Should().Be("SET_TEMPERATURE"); } + // ---- ExportDevicesAsync / JSON ---------------------------------------- + [Fact] - public void BuildExportData_WithBothWhitelists_StacksFilters() + public async Task ExportDevicesAsync_WithEmptyList_ReturnsEmptyJsonArray() { - // Arrange – two ParamSets with different values each - var paramSetValues = new[] - { - CreateParamSetValues("MASTER", - ("BOOST_TIME", 5, "Boost Time"), - ("DECALCIFICATION_TIME", 22, "Decalcification Time")), - CreateParamSetValues("LINK", - ("PEER_NEEDS_BURST", true, "Peer Needs Burst")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - // Allow only MASTER ParamSet and only BOOST_TIME value - var options = new DeviceExportOptions - { - ParamSetWhitelist = ["MASTER"], - ParamValueNameWhitelist = ["BOOST_TIME"] - }; + // Arrange var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var json = await sut.ExportDevicesAsync([]); - // Assert – LINK is filtered out by ParamSetWhitelist, DECALCIFICATION_TIME by ParamValueNameWhitelist - result.ParamSetValues.Should().HaveCount(1); - result.ParamSetValues.First().ParamSetKey.Should().Be("MASTER"); - result.ParamSetValues.First().Values.Should().HaveCount(1); - result.ParamSetValues.First().Values.First().Key.Should().Be("BOOST_TIME"); + // Assert + using var document = JsonDocument.Parse(json); + document.RootElement.ValueKind.Should().Be(JsonValueKind.Array); + document.RootElement.GetArrayLength().Should().Be(0); } [Fact] - public void BuildExportData_WithParamValueNameWhitelistCaseInsensitive_FiltersCorrectly() + public async Task ExportDevicesAsync_WithSingleDevice_ReturnsArrayWithOneElement() { // Arrange - var paramSetValues = new[] - { - CreateParamSetValues("VALUES", - ("SET_TEMPERATURE", 21.5, "Set Temperature"), - ("ACTUAL_TEMPERATURE", 20.1, "Actual Temperature")) - }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["set_temperature"] }; + var device = new CompleteCcuDeviceFakeBuilder().WithName("OnlyDevice").Build(); var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var json = await sut.ExportDevicesAsync([device]); // Assert - result.ParamSetValues.First().Values.Should().HaveCount(1); - result.ParamSetValues.First().Values.First().Key.Should().Be("SET_TEMPERATURE"); + using var document = JsonDocument.Parse(json); + document.RootElement.ValueKind.Should().Be(JsonValueKind.Array); + document.RootElement.GetArrayLength().Should().Be(1); } [Fact] - public void BuildExportData_WithChannelAndParamValueNameWhitelist_FiltersChannelValues() + public async Task ExportDevicesAsync_WithMultipleDevices_PreservesOrder() { // Arrange - var channelParamSetValues = new[] - { - CreateParamSetValues("VALUES", - ("SET_TEMPERATURE", 21.5, "Set Temperature"), - ("ACTUAL_TEMPERATURE", 20.1, "Actual Temperature"), - ("VALVE_STATE", 45, "Valve State")) - }; - var channel = CreateFakeChannel(paramSetValues: channelParamSetValues); - var device = CreateFakeDevice(new FakeDeviceOptions { Channels = [channel] }); - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["SET_TEMPERATURE", "VALVE_STATE"] }; + var device1 = new CompleteCcuDeviceFakeBuilder().WithName("Device1").WithAddress("ADDR1").Build(); + var device2 = new CompleteCcuDeviceFakeBuilder().WithName("Device2").WithAddress("ADDR2").Build(); + var device3 = new CompleteCcuDeviceFakeBuilder().WithName("Device3").WithAddress("ADDR3").Build(); var sut = new DeviceExporter(); // Act - var result = sut.BuildExportData(device, options); + var json = await sut.ExportDevicesAsync([device1, device2, device3]); // Assert - var channelResult = result.Channels.First(); - channelResult.ParamSetValues.First().Values.Should().HaveCount(2); - channelResult.ParamSetValues.First().Values.Select(v => v.Key) - .Should().BeEquivalentTo("SET_TEMPERATURE", "VALVE_STATE"); + var deserialized = JsonSerializer.Deserialize>( + json, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + deserialized.Should().NotBeNull(); + deserialized!.Select(d => d.Name).Should().ContainInOrder("Device1", "Device2", "Device3"); } [Fact] - public async Task ExportDevicesAsync_WithParamValueNameWhitelist_FiltersValuesPerDevice() + public async Task ExportDevicesAsync_WithOptions_AppliesFilterToEachDevice() { // Arrange - var paramSetValues = new[] + var device = new CompleteCcuDeviceFakeBuilder() + .WithParamSet("MASTER", p => p + .Add("BOOST_TIME", 5, "Boost Time") + .Add("DECALCIFICATION_TIME", 22, "Decalcification Time")) + .WithParamSet("LINK", p => p.Add("PEER_NEEDS_BURST", true, "Peer Needs Burst")) + .Build(); + var options = new DeviceExportOptions { - CreateParamSetValues("MASTER", - ("BOOST_TIME", 5, "Boost Time"), - ("DECALCIFICATION_TIME", 22, "Decalcification Time")) + ParamSetWhitelist = ["MASTER"], + ParamValueNameWhitelist = ["BOOST_TIME"] }; - var device = CreateFakeDevice(new FakeDeviceOptions { ParamSetValues = paramSetValues }); - var options = new DeviceExportOptions { ParamValueNameWhitelist = ["BOOST_TIME"] }; var sut = new DeviceExporter(); // Act var json = await sut.ExportDevicesAsync([device], options); // Assert - json.Should().Contain("BOOST_TIME"); - json.Should().NotContain("DECALCIFICATION_TIME"); + var deserialized = JsonSerializer.Deserialize>( + json, + new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + deserialized.Should().NotBeNull(); + var paramSet = deserialized!.Single().ParamSetValues.Single(); + paramSet.ParamSetKey.Should().Be("MASTER"); + paramSet.Values.Select(v => v.Key).Should().BeEquivalentTo("BOOST_TIME"); } } diff --git a/tests/CreativeCoders.HomeMatic.Tests/Exporting/ParamSetValuesBuilder.cs b/tests/CreativeCoders.HomeMatic.Tests/Exporting/ParamSetValuesBuilder.cs new file mode 100644 index 0000000..ff09b9d --- /dev/null +++ b/tests/CreativeCoders.HomeMatic.Tests/Exporting/ParamSetValuesBuilder.cs @@ -0,0 +1,38 @@ +using CreativeCoders.HomeMatic.Core.Devices; +using CreativeCoders.HomeMatic.XmlRpc.Parameters; + +namespace CreativeCoders.HomeMatic.Tests.Exporting; + +internal sealed class ParamSetValuesBuilder +{ + private readonly List _values = []; + + public ParamSetValuesBuilder Add(string name, object value, string? descriptionId = null) + { + _values.Add(new ParamSetValueWithDescription + { + ParamSetValue = new ParamSetValue { Name = name, Value = value }, + Description = new CcuParameterDescription + { + Id = descriptionId, + DefaultValue = null, + MinValue = null, + MaxValue = null, + Type = null, + DataType = ParameterDataType.Float, + Unit = null, + TabOrder = 0, + Control = null, + ValuesList = [], + SpecialValues = [] + } + }); + + return this; + } + + public IEnumerable Build() + { + return _values; + } +} From 895f395a6e059b94444e3399c5feba551e86b948 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:45:30 +0200 Subject: [PATCH 11/15] feat(core): enhance XML documentation for HomeMatic components - Added detailed XML comments to classes, interfaces, and methods across the `CreativeCoders.HomeMatic` and `CreativeCoders.HomeMatic.Exporting` namespaces. - Improved codebase maintainability and tooling support with structured summaries, parameter descriptions, and examples. --- source/CreativeCoders.HomeMatic/CcuClient.cs | 10 ++++ .../CcuClientFactory.cs | 7 +++ source/CreativeCoders.HomeMatic/CcuDevice.cs | 18 +++++++ .../CreativeCoders.HomeMatic/CcuDeviceBase.cs | 13 +++++ .../CcuDeviceBuilder.cs | 47 +++++++++++++++++++ .../CcuDeviceChannel.cs | 7 +++ .../CompleteCcuDevice.cs | 6 +++ .../CompleteCcuDeviceBuilder.cs | 5 ++ .../CompleteCcuDeviceChannel.cs | 5 ++ .../Exporting/ChannelExportData.cs | 23 +++++++++ .../Exporting/DeviceExportData.cs | 35 ++++++++++++++ .../Exporting/DeviceExporter.cs | 10 ++++ .../Exporting/IDeviceExporter.cs | 22 +++++++++ .../Exporting/ParamSetExportData.cs | 11 +++++ .../Exporting/ParamValueExportData.cs | 15 ++++++ .../HomeMaticServiceCollectionExtensions.cs | 14 ++++++ .../MultiCcuClient.cs | 14 ++++++ .../MultiCcuClientFactory.cs | 7 +++ .../XmlRpcApiConnection.cs | 18 +++++++ 19 files changed, 287 insertions(+) diff --git a/source/CreativeCoders.HomeMatic/CcuClient.cs b/source/CreativeCoders.HomeMatic/CcuClient.cs index 30c15bf..d9853f5 100644 --- a/source/CreativeCoders.HomeMatic/CcuClient.cs +++ b/source/CreativeCoders.HomeMatic/CcuClient.cs @@ -6,11 +6,18 @@ namespace CreativeCoders.HomeMatic; +/// +/// Provides access to the devices of a single HomeMatic CCU by combining the CCU's JSON-RPC and XML-RPC APIs. +/// +/// The JSON-RPC client used to retrieve device metadata such as names. +/// The XML-RPC API connections, keyed by the device kind they serve. +/// The builder used to augment a device with parameter descriptions. public class CcuClient( IHomeMaticJsonRpcClient jsonRpcClient, IDictionary xmlRpcApis, ICompleteCcuDeviceBuilder completeCcuDeviceBuilder) : ICcuClient { + /// public async Task> GetDevicesAsync() { var allDevices = new List(); @@ -57,6 +64,7 @@ private CcuDevice CreateDevice(DeviceDescription deviceDescription, XmlRpcApiCon .Build(); } + /// public async Task GetDeviceAsync(string address) { return (await GetDevicesAsync().ConfigureAwait(false)) @@ -64,6 +72,7 @@ public async Task GetDeviceAsync(string address) ?? throw new KeyNotFoundException($"Device with address '{address}' not found."); } + /// public async Task> GetCompleteDevicesAsync() { var completeDevices = new List(); @@ -76,6 +85,7 @@ public async Task> GetCompleteDevicesAsync() return [..completeDevices]; } + /// public async Task GetCompleteDeviceAsync(string address) { var ccuDevice = await GetDeviceAsync(address).ConfigureAwait(false); diff --git a/source/CreativeCoders.HomeMatic/CcuClientFactory.cs b/source/CreativeCoders.HomeMatic/CcuClientFactory.cs index 673be6a..276873c 100644 --- a/source/CreativeCoders.HomeMatic/CcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic/CcuClientFactory.cs @@ -8,11 +8,18 @@ namespace CreativeCoders.HomeMatic; +/// +/// Creates instances that are wired up with the required XML-RPC and JSON-RPC clients. +/// +/// The builder used to create instances. +/// The builder used to create instances. +/// The service provider used to resolve additional dependencies such as . public class CcuClientFactory( IHomeMaticXmlRpcApiBuilder xmlRpcApiBuilder, IHomeMaticJsonRpcClientBuilder jsonRpcClientBuilder, IServiceProvider serviceProvider) : ICcuClientFactory { + /// public ICcuClient CreateClient(string ccuName, IEnumerable deviceKinds, string host, string userName, string password) { diff --git a/source/CreativeCoders.HomeMatic/CcuDevice.cs b/source/CreativeCoders.HomeMatic/CcuDevice.cs index 9c75f70..b38bb57 100644 --- a/source/CreativeCoders.HomeMatic/CcuDevice.cs +++ b/source/CreativeCoders.HomeMatic/CcuDevice.cs @@ -5,24 +5,42 @@ namespace CreativeCoders.HomeMatic; +/// +/// Represents a HomeMatic device with its channels, combining device-level metadata with parameter-set access. +/// +/// The XML-RPC API used to query parameter-set values and descriptions from the CCU. public class CcuDevice(IHomeMaticXmlRpcApi api) : CcuDeviceBase(api), ICcuDevice { + /// public string Name { get; set; } = string.Empty; + /// public required RxModes RxMode { get; init; } + /// public required int RfAddress { get; init; } + /// public required string Firmware { get; init; } + /// public required string AvailableFirmware { get; init; } + /// public required bool CanBeUpdated { get; init; } + /// public required DeviceFirmwareUpdateState FirmwareUpdateState { get; init; } + /// public required IEnumerable Channels { get; init; } + /// + /// Asynchronously retrieves a single channel by its address. + /// + /// The full address of the channel (for example "ABC0001234:1"). + /// A task that yields the matching . + /// Thrown when no channel with the specified address exists on this device. public Task GetChannelAsync(string channelAddress) { var channel = Channels.FirstOrDefault(x => x.Uri.Address == channelAddress); diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs index 3000ea2..05605ef 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs @@ -4,22 +4,34 @@ namespace CreativeCoders.HomeMatic; +/// +/// Provides the shared base implementation of for HomeMatic devices and channels. +/// +/// The XML-RPC API used to query parameter-set values and descriptions from the CCU. public abstract class CcuDeviceBase(IHomeMaticXmlRpcApi api) : ICcuDeviceBase { + /// public required CcuDeviceUri Uri { get; init; } + /// public required string DeviceType { get; init; } + /// public required bool IsAesActive { get; init; } + /// public required string Interface { get; init; } + /// public required int Version { get; init; } + /// public required bool Roaming { get; init; } + /// public required string[] ParamSets { get; init; } + /// public async Task> GetParamSetValuesAsync(string paramSetKey) { var paramSets = await api.GetParamSetAsync(Uri.Address, paramSetKey).ConfigureAwait(false); @@ -31,6 +43,7 @@ public async Task> GetParamSetValuesAsync(string para }); } + /// public async Task GetParamSetDescriptionsAsync(string paramSetKey) { var paramSetDescriptions = diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs index 44896fd..95bf7a8 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs @@ -10,6 +10,10 @@ namespace CreativeCoders.HomeMatic; +/// +/// Fluent builder that constructs a including its channels from an XML-RPC +/// and the list of all devices that share the same CCU. +/// public class CcuDeviceBuilder : ObjectBuilderBase { #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value @@ -23,9 +27,19 @@ public class CcuDeviceBuilder : ObjectBuilderBase private IEnumerable? _devices; + /// + /// Sets the of the device to build. + /// + /// The URI identifying the device on its CCU. + /// A new instance with the value applied. public CcuDeviceBuilder WithUri(CcuDeviceUri deviceUri) => WithField(x => x._uri, deviceUri); + /// + /// Sets the XML-RPC API that the built device should use for parameter-set access. + /// + /// The XML-RPC API instance. + /// The same instance, to allow chaining calls. public CcuDeviceBuilder WithApi(IHomeMaticXmlRpcApi api) { _api = api; @@ -33,6 +47,11 @@ public CcuDeviceBuilder WithApi(IHomeMaticXmlRpcApi api) return this; } + /// + /// Seeds the builder with a whose values are copied onto the built device. + /// + /// The device description returned by the CCU. + /// The same instance, to allow chaining calls. public CcuDeviceBuilder FromDeviceDescription(DeviceDescription deviceDescription) { _deviceDescription = deviceDescription; @@ -40,6 +59,11 @@ public CcuDeviceBuilder FromDeviceDescription(DeviceDescription deviceDescriptio return this; } + /// + /// Builds the from the previously configured values. + /// + /// A new instance populated with device and channel data. + /// Thrown when , or has not been called. public override CcuDevice Build() { if (_uri == null || _api == null || _devices == null) @@ -102,6 +126,12 @@ private IEnumerable CreateChannelsForDevice(DeviceDescription return [..channels]; } + /// + /// Sets the list of all device descriptions known for the CCU so that the builder can resolve the + /// channels that belong to the device currently being built. + /// + /// All device descriptions of the CCU, including channels. + /// The same instance, to allow chaining calls. public CcuDeviceBuilder WithAllDevices(IEnumerable devices) { _devices = devices; @@ -110,9 +140,22 @@ public CcuDeviceBuilder WithAllDevices(IEnumerable devices) } } +/// +/// Base class for immutable fluent builders that produce a new builder instance on every configuration step. +/// +/// The concrete builder type. Used so that WithField returns a new instance of the same type. +/// The type produced by . public abstract class ObjectBuilderBase where TBuilderImpl : class, new() { + /// + /// Creates a new builder instance, copies all private fields from the current instance and applies the + /// supplied value to the selected field. + /// + /// The type of the field being assigned. + /// An expression selecting the private backing field to set. + /// The value to assign to the selected field. + /// A new instance with the updated value. [SuppressMessage("csharpsquid", "S3011", Justification = "Reflection only used for writing own private fields")] protected TBuilderImpl WithField(Expression> property, TProperty value) { @@ -131,5 +174,9 @@ protected TBuilderImpl WithField(Expression + /// Builds the configured object. + /// + /// The built instance. public abstract TOutput Build(); } diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs index 29e3c4a..364486f 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs @@ -4,11 +4,18 @@ namespace CreativeCoders.HomeMatic; +/// +/// Represents a single channel of a HomeMatic device. +/// +/// The XML-RPC API used to query parameter-set values and descriptions from the CCU. public class CcuDeviceChannel(IHomeMaticXmlRpcApi api) : CcuDeviceBase(api), ICcuDeviceChannel { + /// public required int Index { get; init; } + /// public required string Group { get; init; } + /// public required ChannelDirection ChannelDirection { get; init; } } diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs index a3b80ab..1d70a87 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs @@ -2,11 +2,17 @@ namespace CreativeCoders.HomeMatic; +/// +/// Represents a HomeMatic device combined with all its parameter-set values and descriptions. +/// public class CompleteCcuDevice : ICompleteCcuDevice { + /// public required ICcuDeviceData DeviceData { get; init; } + /// public required IEnumerable Channels { get; init; } + /// public required IEnumerable ParamSetValues { get; init; } } diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs index 9880427..e00cff1 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs @@ -4,8 +4,13 @@ namespace CreativeCoders.HomeMatic; +/// +/// Default implementation of that augments an +/// with the parameter-set values and descriptions of its device and channels. +/// public class CompleteCcuDeviceBuilder : ICompleteCcuDeviceBuilder { + /// public async Task BuildAsync(ICcuDevice device) { var channels = await GetChannelsAsync(device).ConfigureAwait(false); diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs index fd277fd..8770a05 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs @@ -2,9 +2,14 @@ namespace CreativeCoders.HomeMatic; +/// +/// Represents a channel combined with all its parameter-set values and descriptions. +/// public class CompleteCcuDeviceChannel : ICompleteCcuDeviceChannel { + /// public required ICcuDeviceChannelData ChannelData { get; init; } + /// public required IEnumerable ParamSetValues { get; init; } } diff --git a/source/CreativeCoders.HomeMatic/Exporting/ChannelExportData.cs b/source/CreativeCoders.HomeMatic/Exporting/ChannelExportData.cs index 18ba583..d18ce64 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/ChannelExportData.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/ChannelExportData.cs @@ -1,14 +1,37 @@ namespace CreativeCoders.HomeMatic.Exporting; +/// +/// Represents the serialized view of a single HomeMatic channel exported by . +/// public class ChannelExportData { + /// + /// Gets the channel address on the CCU. + /// + /// The channel address, including the channel index suffix. public required string Address { get; init; } + /// + /// Gets the device type of the channel as reported by the CCU. + /// + /// The channel's device type string. public required string DeviceType { get; init; } + /// + /// Gets the zero-based index of the channel within its parent device. + /// + /// The channel index. public required int Index { get; init; } + /// + /// Gets the parameter-set keys available for this channel. + /// + /// The array of parameter-set keys. public required string[] ParamSets { get; init; } + /// + /// Gets the parameter-set values of the channel that passed the export filter. + /// + /// The enumerable of entries. public required IEnumerable ParamSetValues { get; init; } } diff --git a/source/CreativeCoders.HomeMatic/Exporting/DeviceExportData.cs b/source/CreativeCoders.HomeMatic/Exporting/DeviceExportData.cs index b7661ee..b4a8679 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/DeviceExportData.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/DeviceExportData.cs @@ -1,20 +1,55 @@ namespace CreativeCoders.HomeMatic.Exporting; +/// +/// Represents the serialized view of a HomeMatic device exported by . +/// public class DeviceExportData { + /// + /// Gets the human-readable name of the device. + /// + /// The device name. public required string Name { get; init; } + /// + /// Gets the device address on the CCU. + /// + /// The device address. public required string Address { get; init; } + /// + /// Gets the device type as reported by the CCU. + /// + /// The device type string. public required string DeviceType { get; init; } + /// + /// Gets the parameter-set keys available for this device. + /// + /// The array of parameter-set keys. public required string[] ParamSetKeys { get; init; } + /// + /// Gets the currently installed firmware version of the device. + /// + /// The firmware version string. public required string FirmwareVersion { get; init; } + /// + /// Gets the display name of the CCU that owns the device. + /// + /// The CCU display name. public required string Ccu { get; init; } + /// + /// Gets the parameter-set values of the device that passed the export filter. + /// + /// The enumerable of entries. public required IEnumerable ParamSetValues { get; init; } + /// + /// Gets the exported channels of the device. + /// + /// The enumerable of entries. public required IEnumerable Channels { get; init; } } diff --git a/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs b/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs index 3ebf381..befef06 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs @@ -4,8 +4,16 @@ namespace CreativeCoders.HomeMatic.Exporting; +/// +/// Default JSON implementation of . +/// +/// +/// The exporter uses with camelCase property naming and skips +/// values. +/// public class DeviceExporter : IDeviceExporter { + /// public Task ExportDeviceAsync(ICompleteCcuDevice device, DeviceExportOptions? options = null) { var exportData = BuildExportData(device, options); @@ -13,6 +21,7 @@ public Task ExportDeviceAsync(ICompleteCcuDevice device, DeviceExportOpt return Task.FromResult(json); } + /// public Task ExportDevicesAsync(IEnumerable devices, DeviceExportOptions? options = null) { var exportDataList = devices.Select(d => BuildExportData(d, options)).ToList(); @@ -20,6 +29,7 @@ public Task ExportDevicesAsync(IEnumerable devices, return Task.FromResult(json); } + /// public DeviceExportData BuildExportData(ICompleteCcuDevice device, DeviceExportOptions? options = null) { return new DeviceExportData diff --git a/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs b/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs index ec92f19..3d266b0 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs @@ -2,11 +2,33 @@ namespace CreativeCoders.HomeMatic.Exporting; +/// +/// Exports data to a serialized representation such as JSON. +/// public interface IDeviceExporter { + /// + /// Asynchronously exports a single device to its serialized representation. + /// + /// The device to export. + /// Optional export options controlling filtering and formatting. + /// A task that yields the serialized representation of the device. Task ExportDeviceAsync(ICompleteCcuDevice device, DeviceExportOptions? options = null); + /// + /// Asynchronously exports a sequence of devices to a single serialized representation. + /// + /// The devices to export. + /// Optional export options controlling filtering and formatting. + /// A task that yields the serialized representation of the devices. Task ExportDevicesAsync(IEnumerable devices, DeviceExportOptions? options = null); + /// + /// Builds the intermediate representation of a device, applying + /// the filter rules from without serializing the result. + /// + /// The device to build the export data for. + /// Optional export options controlling filtering. + /// The filtered for the device. DeviceExportData BuildExportData(ICompleteCcuDevice device, DeviceExportOptions? options = null); } diff --git a/source/CreativeCoders.HomeMatic/Exporting/ParamSetExportData.cs b/source/CreativeCoders.HomeMatic/Exporting/ParamSetExportData.cs index 192032f..d57f8d4 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/ParamSetExportData.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/ParamSetExportData.cs @@ -1,8 +1,19 @@ namespace CreativeCoders.HomeMatic.Exporting; +/// +/// Represents the exported values of a single parameter set of a device or channel. +/// public class ParamSetExportData { + /// + /// Gets the key of the parameter set these values belong to. + /// + /// The parameter-set key (for example "MASTER" or "VALUES"). public required string ParamSetKey { get; init; } + /// + /// Gets the individual parameter values of this parameter set. + /// + /// The enumerable of entries. public required IEnumerable Values { get; init; } } diff --git a/source/CreativeCoders.HomeMatic/Exporting/ParamValueExportData.cs b/source/CreativeCoders.HomeMatic/Exporting/ParamValueExportData.cs index 3263b8d..46f8a54 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/ParamValueExportData.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/ParamValueExportData.cs @@ -1,10 +1,25 @@ namespace CreativeCoders.HomeMatic.Exporting; +/// +/// Represents a single exported parameter value. +/// public class ParamValueExportData { + /// + /// Gets the technical key of the parameter (for example "SET_TEMPERATURE"). + /// + /// The parameter key. public required string Key { get; init; } + /// + /// Gets the descriptive name of the parameter when it differs from . + /// + /// The parameter name, or if it is identical to . public required string? Name { get; init; } + /// + /// Gets the current value of the parameter. + /// + /// The parameter value as reported by the CCU. public required object Value { get; init; } } diff --git a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs index c4b0dbf..8cf447e 100644 --- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs +++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs @@ -7,8 +7,22 @@ namespace CreativeCoders.HomeMatic; +/// +/// Provides extension methods for registering the HomeMatic services on an . +/// public static class HomeMaticServiceCollectionExtensions { + /// + /// Registers the default HomeMatic services including the XML-RPC, JSON-RPC and exporting components. + /// + /// The service collection to register the services on. + /// The same instance, to allow chaining calls. + /// + /// + /// var services = new ServiceCollection(); + /// services.AddHomeMatic(); + /// + /// public static IServiceCollection AddHomeMatic(this IServiceCollection services) { services.AddHomeMaticXmlRpc(); diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs index 47e6cc0..4a65508 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs @@ -5,6 +5,16 @@ namespace CreativeCoders.HomeMatic; +/// +/// Aggregates several instances into a single client that routes per-device +/// calls to the CCU that owns the device. +/// +/// The underlying CCU clients to dispatch calls to. +/// The routing table used to cache the mapping from device address to . +/// +/// The first call to or populates the +/// so that subsequent per-device calls can skip the full scan. +/// public class MultiCcuClient(IEnumerable ccuClients, ICcuRoutingTable routingTable) : IMultiCcuClient { @@ -12,6 +22,7 @@ public class MultiCcuClient(IEnumerable ccuClients, ICcuRoutingTable private readonly ICcuRoutingTable _routingTable = Ensure.NotNull(routingTable); + /// public async Task> GetDevicesAsync() { var results = await GetDataFromClientsAsync(x => x.GetDevicesAsync()).ConfigureAwait(false); @@ -22,6 +33,7 @@ public async Task> GetDevicesAsync() return results.SelectMany(pair => pair.Items); } + /// public Task GetDeviceAsync(string address) { Ensure.IsNotNullOrWhitespace(address); @@ -29,6 +41,7 @@ public Task GetDeviceAsync(string address) return InvokeWithRoutingAsync(address, (client, deviceAddress) => client.GetDeviceAsync(deviceAddress)); } + /// public async Task> GetCompleteDevicesAsync() { var results = await GetDataFromClientsAsync(x => x.GetCompleteDevicesAsync()).ConfigureAwait(false); @@ -39,6 +52,7 @@ public async Task> GetCompleteDevicesAsync() return results.SelectMany(pair => pair.Items); } + /// public Task GetCompleteDeviceAsync(string address) { Ensure.IsNotNullOrWhitespace(address); diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs index 430155e..c64a214 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs @@ -3,10 +3,16 @@ namespace CreativeCoders.HomeMatic; +/// +/// Default implementation of that collects CCU configurations and +/// builds an backed by a . +/// +/// The factory used to create the per-CCU instances. public class MultiCcuClientFactory(ICcuClientFactory ccuClientFactory) : IMultiCcuClientFactory { private readonly List _ccuClients = []; + /// public IMultiCcuClientFactory AddCcu(string ccuName, string host, string userName, string password, params CcuDeviceKind[] deviceKinds) { @@ -17,6 +23,7 @@ public IMultiCcuClientFactory AddCcu(string ccuName, string host, string userNam return this; } + /// public IMultiCcuClient Build() { return new MultiCcuClient(_ccuClients, new CcuRoutingTable()); diff --git a/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs b/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs index b115737..467378e 100644 --- a/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs +++ b/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs @@ -4,11 +4,29 @@ namespace CreativeCoders.HomeMatic; +/// +/// Bundles an and the associated +/// together with the logical CCU name. +/// +/// The address identifying the XML-RPC endpoint on the CCU. +/// The XML-RPC API instance used to talk to that endpoint. public class XmlRpcApiConnection(XmlRpcApiAddress address, IHomeMaticXmlRpcApi api) { + /// + /// Gets the address of the XML-RPC endpoint on the CCU. + /// + /// The of the endpoint. public XmlRpcApiAddress Address { get; } = Ensure.NotNull(address); + /// + /// Gets or sets the logical name of the CCU this connection belongs to. + /// + /// The CCU name, or an empty string if not set. public string CcuName { get; set; } = string.Empty; + /// + /// Gets the XML-RPC API instance used to communicate with the CCU endpoint. + /// + /// The instance. public IHomeMaticXmlRpcApi Api { get; } = Ensure.NotNull(api); } From 05bfac9b75a2dcaac47d637dc26daeeafc02ac24 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:53:34 +0200 Subject: [PATCH 12/15] refactor: clean up namespaces and enhance XML documentation - Removed unused namespaces across multiple files. - Suppressed warnings for Inheritdoc usage in exception and class summaries. - Applied `inheritdoc` annotations for consistent XML documentation. - Refactored redundant lambda usage and adjusted collection initialization syntax. --- build/BuildContext.cs | 1 - .../Devices/ICcuDeviceChannel.cs | 4 +--- .../Devices/ICcuDeviceData.cs | 1 - .../Parameters/ParamSetKey.cs | 4 ++-- .../HomeMaticJsonRpcClient.cs | 4 ++-- .../Exceptions/UnknownParamSetException.cs | 2 ++ source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs | 7 ++++--- source/CreativeCoders.HomeMatic/CcuRoutingTable.cs | 9 ++++----- source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs | 1 + .../CompleteCcuDeviceBuilder.cs | 3 ++- .../CompleteCcuDeviceChannel.cs | 1 + source/CreativeCoders.HomeMatic/MultiCcuClient.cs | 10 +++++----- .../CreativeCoders.HomeMatic/MultiCcuClientFactory.cs | 7 ++++--- .../Commanding/CliBaseCommand.cs | 5 ++--- .../CcuClientFactoryTests.cs | 2 +- tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs | 10 +++++----- .../HomeMaticServiceCollectionExtensionsTests.cs | 2 +- 17 files changed, 37 insertions(+), 36 deletions(-) diff --git a/build/BuildContext.cs b/build/BuildContext.cs index 057ea20..e1f82fc 100644 --- a/build/BuildContext.cs +++ b/build/BuildContext.cs @@ -6,7 +6,6 @@ using CreativeCoders.CakeBuild.Tasks.Templates.Settings; using CreativeCoders.Core; using CreativeCoders.Core.Collections; -using CreativeCoders.Core.IO; using JetBrains.Annotations; namespace Build; diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs index 7256f75..98c95dd 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs @@ -3,6 +3,4 @@ namespace CreativeCoders.HomeMatic.Core.Devices; /// /// Represents a single channel of a HomeMatic device. /// -public interface ICcuDeviceChannel : ICcuDeviceBase, ICcuDeviceChannelData -{ -} +public interface ICcuDeviceChannel : ICcuDeviceBase, ICcuDeviceChannelData; diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs index d18a4ad..2cb00e3 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs @@ -1,4 +1,3 @@ -using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Devices; using CreativeCoders.HomeMatic.XmlRpc.Parameters; diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs index df19a4c..5ef40a0 100644 --- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs +++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs @@ -1,4 +1,4 @@ -using JetBrains.Annotations; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core.Parameters; @@ -31,7 +31,7 @@ public static class ParamSetKey /// /// All known parameter-set keys. /// - public static readonly string[] ParamSetKeys = {Master, Values, Link, Service}; + public static readonly string[] ParamSetKeys = [Master, Values, Link, Service]; /// /// Converts a parameter-set key string into the corresponding value. diff --git a/source/CreativeCoders.HomeMatic.JsonRpc/HomeMaticJsonRpcClient.cs b/source/CreativeCoders.HomeMatic.JsonRpc/HomeMaticJsonRpcClient.cs index 14da292..745143c 100644 --- a/source/CreativeCoders.HomeMatic.JsonRpc/HomeMaticJsonRpcClient.cs +++ b/source/CreativeCoders.HomeMatic.JsonRpc/HomeMaticJsonRpcClient.cs @@ -10,7 +10,7 @@ namespace CreativeCoders.HomeMatic.JsonRpc; public class HomeMaticJsonRpcClient(IHomeMaticJsonRpcApi jsonRpcApi) : IHomeMaticJsonRpcClient { private readonly IHomeMaticJsonRpcApi _jsonRpcApi = Ensure.NotNull(jsonRpcApi); - private readonly SynchronizedValue _sessionId = SynchronizedValue.Create(null); + private readonly SynchronizedValue _sessionId = SynchronizedValue.Create(null); public async Task LoginAsync() { @@ -41,7 +41,7 @@ public async Task> ListAllDetailsAsync() var jsonRpcResponse = await InvokeAsync(sessionId => _jsonRpcApi.ListAllDetailsAsync(sessionId)) .ConfigureAwait(false); - return jsonRpcResponse.Result ?? Array.Empty(); + return jsonRpcResponse.Result ?? []; } public IAsyncDisposable AutoLogout() diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs index 85163cb..a8917d4 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs @@ -1,10 +1,12 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace CreativeCoders.HomeMatic.XmlRpc.Exceptions; /// /// The exception that is thrown when the CCU does not recognize the requested parameter set. /// +[SuppressMessage("ReSharper", "InheritdocConsiderUsage")] public class UnknownParamSetException : CcuXmlRpcException { /// diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs index 95bf7a8..6532223 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs @@ -59,11 +59,12 @@ public CcuDeviceBuilder FromDeviceDescription(DeviceDescription deviceDescriptio return this; } + /// /// - /// Builds the from the previously configured values. + /// Builds the CcuDevice from the previously configured values. /// - /// A new instance populated with device and channel data. - /// Thrown when , or has not been called. + /// A new CcuDevice instance populated with device and channel data. + /// Thrown when WithUri, WithApi or WithAllDevices has not been called. public override CcuDevice Build() { if (_uri == null || _api == null || _devices == null) diff --git a/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs b/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs index 8be85b5..717526a 100644 --- a/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs +++ b/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs @@ -4,9 +4,10 @@ namespace CreativeCoders.HomeMatic; +/// /// -/// Default thread-safe implementation of backed by a -/// . +/// Default thread-safe implementation of ICcuRoutingTable backed by a +/// ConcurrentDictionary{TKey,TValue}. /// public class CcuRoutingTable : ICcuRoutingTable { @@ -32,9 +33,7 @@ public void Register(string address, ICcuClient client) /// public void Register(IEnumerable> entries) { - Ensure.NotNull(entries); - - foreach (var entry in entries) + foreach (var entry in Ensure.NotNull(entries)) { Register(entry.Key, entry.Value); } diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs index 1d70a87..544faad 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs @@ -2,6 +2,7 @@ namespace CreativeCoders.HomeMatic; +/// /// /// Represents a HomeMatic device combined with all its parameter-set values and descriptions. /// diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs index e00cff1..6d60d58 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs @@ -4,8 +4,9 @@ namespace CreativeCoders.HomeMatic; +/// /// -/// Default implementation of that augments an +/// Default implementation of ICompleteCcuDeviceBuilder that augments an ICcuDevice /// with the parameter-set values and descriptions of its device and channels. /// public class CompleteCcuDeviceBuilder : ICompleteCcuDeviceBuilder diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs index 8770a05..cb5b2c6 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs @@ -2,6 +2,7 @@ namespace CreativeCoders.HomeMatic; +/// /// /// Represents a channel combined with all its parameter-set values and descriptions. /// diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs index 4a65508..48de712 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs @@ -1,19 +1,19 @@ -using System.Diagnostics.CodeAnalysis; using CreativeCoders.Core; using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Core.Devices; namespace CreativeCoders.HomeMatic; +/// /// -/// Aggregates several instances into a single client that routes per-device +/// Aggregates several ICcuClient instances into a single client that routes per-device /// calls to the CCU that owns the device. /// /// The underlying CCU clients to dispatch calls to. -/// The routing table used to cache the mapping from device address to . +/// The routing table used to cache the mapping from device address to ICcuClient. /// -/// The first call to or populates the -/// so that subsequent per-device calls can skip the full scan. +/// The first call to GetDevicesAsync or GetCompleteDevicesAsync populates the +/// so that subsequent per-device calls can skip the full scan. /// public class MultiCcuClient(IEnumerable ccuClients, ICcuRoutingTable routingTable) : IMultiCcuClient diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs index c64a214..45e4697 100644 --- a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs +++ b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs @@ -3,11 +3,12 @@ namespace CreativeCoders.HomeMatic; +/// /// -/// Default implementation of that collects CCU configurations and -/// builds an backed by a . +/// Default implementation of IMultiCcuClientFactory that collects CCU configurations and +/// builds an IMultiCcuClient backed by a CcuRoutingTable. /// -/// The factory used to create the per-CCU instances. +/// The factory used to create the per-CCU ICcuClient instances. public class MultiCcuClientFactory(ICcuClientFactory ccuClientFactory) : IMultiCcuClientFactory { private readonly List _ccuClients = []; diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs index e6163cd..55f68b5 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs @@ -1,5 +1,4 @@ using CreativeCoders.Core; -using CreativeCoders.HomeMatic.Core; using CreativeCoders.HomeMatic.Tools.Cli.Base.SharedData; using CreativeCoders.HomeMatic.XmlRpc; using CreativeCoders.HomeMatic.XmlRpc.Client; @@ -15,11 +14,11 @@ protected CliBaseCommand(IHomeMaticXmlRpcApiBuilder apiBuilder, ISharedData shar _apiBuilder = Ensure.NotNull(apiBuilder); SharedData = Ensure.NotNull(sharedData); } - + protected IHomeMaticXmlRpcApi BuildApi() { var cliData = SharedData.LoadCliData(); - + return _apiBuilder .ForUrl(new Uri($"http://{cliData.CcuHost}:{CcuRpcPorts.HomeMaticIp}")) .Build(); diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs index 923d5d6..a62f219 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs @@ -43,7 +43,7 @@ public void CreateClient_ShouldReturnCcuClient_WhenCalled() A.CallTo(() => xmlRpcApiBuilder.ForUrl(new Uri($"http://example.com:{CcuRpcPorts.CoupledDevices}"))) .MustNotHaveHappened(); - A.CallTo(() => jsonRpcApiBuilder.ForUrl(new Uri($"http://example.com"))) + A.CallTo(() => jsonRpcApiBuilder.ForUrl(new Uri("http://example.com"))) .MustHaveHappenedOnceExactly(); ccuClient diff --git a/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs index 0ed930f..1da518c 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/CcuClientTests.cs @@ -68,7 +68,7 @@ public async Task GetDevicesAsync_HomeMaticDevicesAvailable_ReturnsCcuDevices() Address = "1234567890", Interface = "BidCos-RF", Type = "HmIP-SWDO", - Name = "TestDevice0", + Name = "TestDevice0" } ]; @@ -123,7 +123,7 @@ public async Task GetDevicesAsync_HomeMaticIpDevicesAvailable_ReturnsCcuDevices( Address = "9876543210", Interface = "HmIP-RF", Type = "HmIP-SWDO", - Name = "TestDeviceIp", + Name = "TestDeviceIp" } ]; @@ -181,14 +181,14 @@ public async Task GetDevicesAsync_MixedDevicesAvailable_ReturnsAllCcuDevices() Address = "1234567890", Interface = "BidCos-RF", Type = "HmIP-SWDO", - Name = "TestDevice0", + Name = "TestDevice0" }, new DeviceDetails { Address = "9876543210", Interface = "HmIP-RF", Type = "HmIP-SWDO", - Name = "TestDeviceIp", + Name = "TestDeviceIp" } ]; @@ -489,7 +489,7 @@ public async Task GetCompleteDevicesAsync_BuilderThrows_PropagatesException() var ccuClient = CreateCcuClient(jsonRpcClient, homeMaticXmlRpcApi, homeMaticIpXmlRpcApi, completeBuilder); // Act - var act = () => ccuClient.GetCompleteDevicesAsync(); + var act = ccuClient.GetCompleteDevicesAsync; // Assert await act.Should().ThrowAsync().WithMessage("boom"); diff --git a/tests/CreativeCoders.HomeMatic.Tests/HomeMaticServiceCollectionExtensionsTests.cs b/tests/CreativeCoders.HomeMatic.Tests/HomeMaticServiceCollectionExtensionsTests.cs index 6def372..5e25890 100644 --- a/tests/CreativeCoders.HomeMatic.Tests/HomeMaticServiceCollectionExtensionsTests.cs +++ b/tests/CreativeCoders.HomeMatic.Tests/HomeMaticServiceCollectionExtensionsTests.cs @@ -10,7 +10,7 @@ public class HomeMaticServiceCollectionExtensionsTests public void GetRequiredService_CcuClientSupportAdded_GetCcuClientFactory() { // Arrange - var services = new ServiceCollection() as IServiceCollection; + IServiceCollection services = new ServiceCollection(); services.AddHomeMatic(); var sp = services.BuildServiceProvider(); From 00e692b60001fee06aa268f2d707fda236302fe6 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:04:23 +0200 Subject: [PATCH 13/15] refactor: improve type and collection definitions, add annotations - Updated return type in `BuildParamSetExportData` from `IEnumerable` to an array for consistency. - Applied `[PublicAPI]` annotations to interfaces, classes, and extensions for improved tooling support. - Simplified `CcuDevice` and `ParamSetValuesWithDescriptions` initialization expressions. - Adjusted `.gitignore` and `.editorconfig` with new entries for project customization. --- .editorconfig | 1 + .gitignore | 1 + HomeMatic.sln | 1 + .../Devices/ICcuDeviceData.cs | 2 ++ source/CreativeCoders.HomeMatic/CcuClient.cs | 7 ++----- source/CreativeCoders.HomeMatic/CcuDeviceBase.cs | 2 +- source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs | 2 +- .../CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs | 4 ++-- .../Exporting/DeviceExportOptions.cs | 3 +++ .../CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs | 4 ++-- .../HomeMaticServiceCollectionExtensions.cs | 2 ++ 11 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.editorconfig b/.editorconfig index c572378..25768fd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,4 @@ trim_trailing_whitespace = true # IDE0060: Remove unused parameter dotnet_diagnostic.IDE0060.severity = warning configure_await_analysis_mode = library +resharper_inheritdoc_consider_usage_highlighting = none diff --git a/.gitignore b/.gitignore index 0c1ba97..19b8cbb 100644 --- a/.gitignore +++ b/.gitignore @@ -324,3 +324,4 @@ ASALocalRun/ .artifacts .tests +.tokensave diff --git a/HomeMatic.sln b/HomeMatic.sln index cab3077..9b7c40b 100644 --- a/HomeMatic.sln +++ b/HomeMatic.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__global", "__global", "{73 GitVersion.yml = GitVersion.yml Directory.Packages.props = Directory.Packages.props .github\dependabot.yml = .github\dependabot.yml + .editorconfig = .editorconfig EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cli", "Cli", "{B79F3B3E-C9CE-4629-ADE3-B1659AF9C673}" diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs index 2cb00e3..c87e201 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs @@ -1,11 +1,13 @@ using CreativeCoders.HomeMatic.XmlRpc.Devices; using CreativeCoders.HomeMatic.XmlRpc.Parameters; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core.Devices; /// /// Defines the device-level data of a HomeMatic device. /// +[PublicAPI] public interface ICcuDeviceData : ICcuDeviceBaseData { /// diff --git a/source/CreativeCoders.HomeMatic/CcuClient.cs b/source/CreativeCoders.HomeMatic/CcuClient.cs index d9853f5..ba478b1 100644 --- a/source/CreativeCoders.HomeMatic/CcuClient.cs +++ b/source/CreativeCoders.HomeMatic/CcuClient.cs @@ -38,16 +38,13 @@ public async Task> GetDevicesAsync() var device = allDevices.FirstOrDefault(d => d.Uri.Address.Equals(x.Address, StringComparison.OrdinalIgnoreCase)); - if (device != null) - { - device.Name = x?.Name ?? string.Empty; - } + device?.Name = x?.Name ?? string.Empty; }); return [..allDevices]; } - private CcuDevice CreateDevice(DeviceDescription deviceDescription, XmlRpcApiConnection xmlRpcApiConnection, + private static CcuDevice CreateDevice(DeviceDescription deviceDescription, XmlRpcApiConnection xmlRpcApiConnection, IEnumerable allDevices) { return new CcuDeviceBuilder() diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs index 05605ef..21e2dcd 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBase.cs @@ -49,7 +49,7 @@ public async Task GetParamSetDescriptionsAsync(string var paramSetDescriptions = await api.GetParameterDescriptionAsync(Uri.Address, paramSetKey).ConfigureAwait(false); - return new CcuParameterDescriptions() + return new CcuParameterDescriptions { ParamSetKey = paramSetKey, Items = paramSetDescriptions diff --git a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs index 6532223..1d49622 100644 --- a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs @@ -87,7 +87,7 @@ public override CcuDevice Build() FirmwareUpdateState = _deviceDescription?.FirmwareUpdateState ?? DeviceFirmwareUpdateState.None, Roaming = _deviceDescription?.Roaming ?? false, ParamSets = _deviceDescription?.ParamSets ?? [], - Channels = CreateChannelsForDevice(_deviceDescription, _devices), + Channels = CreateChannelsForDevice(_deviceDescription, _devices) }; return ccuDevice; diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs index 6d60d58..9476e9d 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs @@ -44,7 +44,7 @@ private async Task> GetChannelsAsync(ICcu return [..channels]; } - private async Task> GetParamSetValuesAsync(ICcuDeviceBase device) + private static async Task> GetParamSetValuesAsync(ICcuDeviceBase device) { var paramSetValues = new List(); @@ -60,7 +60,7 @@ private async Task> GetParamSetValue throw new KeyNotFoundException() }); - paramSetValues.Add(new ParamSetValuesWithDescriptions() + paramSetValues.Add(new ParamSetValuesWithDescriptions { ParamSetKey = paramSetKey, ParamSetValues = paramSets diff --git a/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs b/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs index cdb7ebe..d277603 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace CreativeCoders.HomeMatic.Exporting; +[PublicAPI] public class DeviceExportOptions { /// diff --git a/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs b/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs index befef06..881970c 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/DeviceExporter.cs @@ -59,7 +59,7 @@ private static ChannelExportData BuildChannelExportData( }; } - private static IEnumerable BuildParamSetExportData( + private static ParamSetExportData[] BuildParamSetExportData( IEnumerable paramSetValues, DeviceExportOptions? options) { @@ -77,7 +77,7 @@ private static IEnumerable BuildParamSetExportData( Value = v.ParamSetValue.Value }).ToList() }) - .ToList(); + .ToArray(); } private static string Serialize(T data, DeviceExportOptions? options) diff --git a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs index 8cf447e..18a11c9 100644 --- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs +++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using CreativeCoders.HomeMatic.Exporting; using CreativeCoders.HomeMatic.JsonRpc; using CreativeCoders.HomeMatic.XmlRpc; +using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -10,6 +11,7 @@ namespace CreativeCoders.HomeMatic; /// /// Provides extension methods for registering the HomeMatic services on an . /// +[PublicAPI] public static class HomeMaticServiceCollectionExtensions { /// From f1f3dda6efd6fde4c539dde4d142a65ad1ead5b9 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:06:34 +0200 Subject: [PATCH 14/15] refactor(core, cli): apply `[PublicAPI]` and suppressions, enhance type safety - Added `[PublicAPI]` annotations to interfaces and classes for improved tooling and API usability. - Applied `[SuppressMessage]` attributes for static analysis in methods and converters. - Improved type safety with `in` keyword and adapted property initializations in multiple constructors. - Streamlined collection initialization syntax and adjusted method signatures for consistency. --- .../Devices/ICcuDeviceBaseData.cs | 3 ++ .../Devices/ICcuDeviceChannelData.cs | 2 ++ .../ICcuRoutingTable.cs | 2 ++ .../IMultiCcuClient.cs | 2 ++ .../IHomeMaticJsonRpcClient.cs | 10 +++--- .../Models/BooleanConverter.cs | 4 ++- .../Models/DeviceDetails.cs | 3 +- .../Converters/LinkRolesValueConverter.cs | 4 +-- .../ParameterDataTypeValueConverter.cs | 34 ++++++++----------- .../DeviceDescription.cs | 11 +++--- .../Server/CcuXmlRpcEventServer.cs | 6 ++-- .../Server/ICcuXmlRpcEventServer.cs | 4 ++- .../CompleteCcuDeviceBuilder.cs | 2 +- .../Exporting/IDeviceExporter.cs | 2 ++ .../Commanding/CliBaseCommand.cs | 12 ++----- .../IHomeMaticCliCommandWithOptions.cs | 2 +- .../Connections/CcuConnectionInfo.cs | 14 +++----- .../Connections/CcuConnectionsStore.cs | 2 +- .../Connections/CliHomeMaticClientBuilder.cs | 4 +-- .../SharedData/DefaultSharedData.cs | 4 +-- 20 files changed, 65 insertions(+), 62 deletions(-) diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs index 9816386..28d476e 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs @@ -1,8 +1,11 @@ +using JetBrains.Annotations; + namespace CreativeCoders.HomeMatic.Core.Devices; /// /// Defines the common identifying and structural data of a HomeMatic device or channel. /// +[PublicAPI] public interface ICcuDeviceBaseData { /// diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs index 981a7c0..3ccdfb8 100644 --- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs +++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs @@ -1,10 +1,12 @@ using CreativeCoders.HomeMatic.XmlRpc.Devices; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core.Devices; /// /// Defines the channel-specific data of a HomeMatic device channel. /// +[PublicAPI] public interface ICcuDeviceChannelData : ICcuDeviceBaseData { /// diff --git a/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs b/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs index ab2a027..aa298dd 100644 --- a/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs +++ b/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core; @@ -9,6 +10,7 @@ namespace CreativeCoders.HomeMatic.Core; /// The routing table enables implementations to avoid querying every configured /// CCU for each per-device call. Implementations must be safe for concurrent use. /// +[PublicAPI] public interface ICcuRoutingTable { /// diff --git a/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs b/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs index 1a6a294..f211367 100644 --- a/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs +++ b/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs @@ -1,12 +1,14 @@ using System.Collections.Generic; using System.Threading.Tasks; using CreativeCoders.HomeMatic.Core.Devices; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Core; /// /// Provides unified access to devices across multiple HomeMatic CCUs. /// +[PublicAPI] public interface IMultiCcuClient { /// diff --git a/source/CreativeCoders.HomeMatic.JsonRpc/IHomeMaticJsonRpcClient.cs b/source/CreativeCoders.HomeMatic.JsonRpc/IHomeMaticJsonRpcClient.cs index 7c07ffe..b9d9d67 100644 --- a/source/CreativeCoders.HomeMatic.JsonRpc/IHomeMaticJsonRpcClient.cs +++ b/source/CreativeCoders.HomeMatic.JsonRpc/IHomeMaticJsonRpcClient.cs @@ -1,17 +1,19 @@ using System.Net; using CreativeCoders.HomeMatic.JsonRpc.Models; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.JsonRpc; +[PublicAPI] public interface IHomeMaticJsonRpcClient { Task LoginAsync(); - + Task LogoutAsync(); - + Task> ListAllDetailsAsync(); IAsyncDisposable AutoLogout(); - + NetworkCredential? Credential { get; set; } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.JsonRpc/Models/BooleanConverter.cs b/source/CreativeCoders.HomeMatic.JsonRpc/Models/BooleanConverter.cs index 227cd93..bb99a0b 100644 --- a/source/CreativeCoders.HomeMatic.JsonRpc/Models/BooleanConverter.cs +++ b/source/CreativeCoders.HomeMatic.JsonRpc/Models/BooleanConverter.cs @@ -1,10 +1,12 @@ -using System.Text.Json; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization; namespace CreativeCoders.HomeMatic.JsonRpc.Models; public class BooleanConverter : JsonConverter { + [SuppressMessage("ReSharper", "SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault")] public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return reader.TokenType switch diff --git a/source/CreativeCoders.HomeMatic.JsonRpc/Models/DeviceDetails.cs b/source/CreativeCoders.HomeMatic.JsonRpc/Models/DeviceDetails.cs index 7b618ab..0efd3df 100644 --- a/source/CreativeCoders.HomeMatic.JsonRpc/Models/DeviceDetails.cs +++ b/source/CreativeCoders.HomeMatic.JsonRpc/Models/DeviceDetails.cs @@ -4,6 +4,7 @@ namespace CreativeCoders.HomeMatic.JsonRpc.Models; [UsedImplicitly] +[PublicAPI] public class DeviceDetails { public string? Id { get; set; } @@ -21,4 +22,4 @@ public class DeviceDetails [JsonConverter(typeof(BooleanConverter))] public bool IsReady { get; set; } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/LinkRolesValueConverter.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/LinkRolesValueConverter.cs index cd4713e..ff1eb9d 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/LinkRolesValueConverter.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/LinkRolesValueConverter.cs @@ -25,7 +25,7 @@ public class LinkRolesValueConverter : IXmlRpcMemberValueConverter { var text = xmlRpcValue.GetValue(); - return text?.Split(new []{" "}, StringSplitOptions.RemoveEmptyEntries); + return text?.Split([" "], StringSplitOptions.RemoveEmptyEntries); } /// @@ -42,4 +42,4 @@ public XmlRpcValue ConvertFromObject(object value) return new StringValue(string.Empty); } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs index 9ccfdbd..167c2f6 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Converters/ParameterDataTypeValueConverter.cs @@ -20,16 +20,17 @@ namespace CreativeCoders.HomeMatic.XmlRpc.Converters; [UsedImplicitly] public class ParameterDataTypeValueConverter : IXmlRpcMemberValueConverter { - private static readonly IDictionary DataTypeMapping = new Dictionary - { - {"INTEGER", ParameterDataType.Integer}, - {"BOOL", ParameterDataType.Bool}, - {"FLOAT", ParameterDataType.Float}, - {"ACTION", ParameterDataType.Action}, - {"ENUM", ParameterDataType.Enum}, - {"STRING", ParameterDataType.String} - }; - + private static readonly Dictionary DataTypeMapping = + new Dictionary + { + { "INTEGER", ParameterDataType.Integer }, + { "BOOL", ParameterDataType.Bool }, + { "FLOAT", ParameterDataType.Float }, + { "ACTION", ParameterDataType.Action }, + { "ENUM", ParameterDataType.Enum }, + { "STRING", ParameterDataType.String } + }; + /// /// Converts an containing a type name string into a value. /// @@ -37,14 +38,9 @@ public class ParameterDataTypeValueConverter : IXmlRpcMemberValueConverter /// The corresponding , or if the value is not a recognized type string. public object ConvertFromValue(XmlRpcValue xmlRpcValue) { - if (xmlRpcValue is not StringValue text) - { - return ParameterDataType.Unknown; - } - - return DataTypeMapping.TryGetValue(text.Value, out var dataType) - ? dataType - : ParameterDataType.Unknown; + return xmlRpcValue is not StringValue text + ? ParameterDataType.Unknown + : DataTypeMapping.GetValueOrDefault(text.Value, ParameterDataType.Unknown); } /// @@ -57,4 +53,4 @@ public XmlRpcValue ConvertFromObject(object value) { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs b/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs index 866f6fa..af0aa96 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using CreativeCoders.HomeMatic.XmlRpc.Devices; using CreativeCoders.HomeMatic.XmlRpc.Parameters; using CreativeCoders.HomeMatic.XmlRpc.Converters; @@ -44,7 +43,7 @@ public class DeviceDescription /// /// An array of channel addresses. Empty for channel entries. [XmlRpcStructMember("CHILDREN")] - public string[] Children { get; set; } = Array.Empty(); + public string[] Children { get; set; } = []; /// /// Gets or sets the address of the parent device. @@ -99,7 +98,7 @@ public class DeviceDescription /// An array of parameter set keys, typically containing MASTER, VALUES, and/or LINK. /// [XmlRpcStructMember("PARAMSETS", DefaultValue = new string[0])] - public string[] ParamSets { get; set; } = Array.Empty(); + public string[] ParamSets { get; set; } = []; /// /// Gets or sets the reception mode flags for this device. Only present for BidCoS-RF devices. @@ -177,14 +176,14 @@ public class DeviceDescription /// A collection of role names (e.g. SWITCH) separated by spaces in the raw XML-RPC data. [XmlRpcStructMember("LINK_SOURCE_ROLES", DefaultValue = new string[0], Converter = typeof(LinkRolesValueConverter))] - public IEnumerable LinkSourceRoles { get; set; } = Array.Empty(); + public IEnumerable LinkSourceRoles { get; set; } = []; /// /// Gets or sets the roles this channel can assume as a receiver in a direct device link. Only present for channels. /// /// A collection of role names (e.g. SWITCH) separated by spaces in the raw XML-RPC data. [XmlRpcStructMember("LINK_TARGET_ROLES", DefaultValue = new string[0], Converter = typeof(LinkRolesValueConverter))] - public IEnumerable LinkTargetRoles { get; set; } = Array.Empty(); + public IEnumerable LinkTargetRoles { get; set; } = []; /// /// Gets a value that indicates whether this description represents a top-level device (not a channel). diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Server/CcuXmlRpcEventServer.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Server/CcuXmlRpcEventServer.cs index 540166d..8a93de2 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Server/CcuXmlRpcEventServer.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Server/CcuXmlRpcEventServer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using CreativeCoders.Core; using CreativeCoders.Core.Collections; @@ -22,13 +23,14 @@ namespace CreativeCoders.HomeMatic.XmlRpc.Server; /// Register with the CCU using Client.IHomeMaticXmlRpcApi.InitAsync after starting. /// [PublicAPI] +[SuppressMessage("Performance", "CA1873:Avoid potentially expensive logging")] public class CcuXmlRpcEventServer : ICcuXmlRpcEventServer { private readonly IXmlRpcServer _xmlRpcServer; private readonly ILogger _logger; - private readonly IList _eventHandlers; + private readonly ConcurrentList _eventHandlers; /// /// Initializes a new instance of the class. @@ -99,7 +101,7 @@ private Task> ListDevices(string interfaceId) { _logger.LogDebug("List devices for InterfaceId = {InterfaceId}", interfaceId); - return Task.FromResult(Array.Empty() as IEnumerable); + return Task.FromResult>([]); } /// diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Server/ICcuXmlRpcEventServer.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Server/ICcuXmlRpcEventServer.cs index 02f368f..f0261e6 100644 --- a/source/CreativeCoders.HomeMatic.XmlRpc/Server/ICcuXmlRpcEventServer.cs +++ b/source/CreativeCoders.HomeMatic.XmlRpc/Server/ICcuXmlRpcEventServer.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.XmlRpc.Server; @@ -11,6 +12,7 @@ namespace CreativeCoders.HomeMatic.XmlRpc.Server; /// Register with the CCU by calling with the /// server's URL and an interface identifier. /// +[PublicAPI] public interface ICcuXmlRpcEventServer { /// @@ -36,4 +38,4 @@ public interface ICcuXmlRpcEventServer /// /// The listen URL (e.g. http://0.0.0.0:5000/). Must be set before calling . string ServerUrl { get; set; } -} \ No newline at end of file +} diff --git a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs index 9476e9d..06b22a8 100644 --- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs +++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs @@ -26,7 +26,7 @@ public async Task BuildAsync(ICcuDevice device) return completeDevice; } - private async Task> GetChannelsAsync(ICcuDevice device) + private static async Task> GetChannelsAsync(ICcuDevice device) { var channels = new List(); diff --git a/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs b/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs index 3d266b0..24440f4 100644 --- a/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs +++ b/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs @@ -1,10 +1,12 @@ using CreativeCoders.HomeMatic.Core.Devices; +using JetBrains.Annotations; namespace CreativeCoders.HomeMatic.Exporting; /// /// Exports data to a serialized representation such as JSON. /// +[PublicAPI] public interface IDeviceExporter { /// diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs index 55f68b5..af703c9 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs @@ -5,15 +5,9 @@ namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding; -public abstract class CliBaseCommand +public abstract class CliBaseCommand(IHomeMaticXmlRpcApiBuilder apiBuilder, ISharedData sharedData) { - private readonly IHomeMaticXmlRpcApiBuilder _apiBuilder; - - protected CliBaseCommand(IHomeMaticXmlRpcApiBuilder apiBuilder, ISharedData sharedData) - { - _apiBuilder = Ensure.NotNull(apiBuilder); - SharedData = Ensure.NotNull(sharedData); - } + private readonly IHomeMaticXmlRpcApiBuilder _apiBuilder = Ensure.NotNull(apiBuilder); protected IHomeMaticXmlRpcApi BuildApi() { @@ -24,5 +18,5 @@ protected IHomeMaticXmlRpcApi BuildApi() .Build(); } - protected ISharedData SharedData { get; set; } + protected ISharedData SharedData { get; } = Ensure.NotNull(sharedData); } diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommandWithOptions.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommandWithOptions.cs index e675839..28338f8 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommandWithOptions.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommandWithOptions.cs @@ -1,6 +1,6 @@ namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding; -public interface IHomeMaticCliCommandWithOptions +public interface IHomeMaticCliCommandWithOptions where TOptions : class { Task ExecuteAsync(TOptions options); diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionInfo.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionInfo.cs index f93bf90..f30eb4e 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionInfo.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionInfo.cs @@ -2,15 +2,9 @@ namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Connections; -public class CcuConnectionInfo +public class CcuConnectionInfo(Uri url, string? name) { - public CcuConnectionInfo(Uri url, string? name) - { - Url = Ensure.NotNull(url); - Name = name ?? url.Host; - } - - public string Name { get; set; } + public string Name { get; set; } = name ?? url.Host; - public Uri Url { get; set; } -} \ No newline at end of file + public Uri Url { get; set; } = Ensure.NotNull(url); +} diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionsStore.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionsStore.cs index b90e2f0..1e8af1e 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionsStore.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CcuConnectionsStore.cs @@ -119,7 +119,7 @@ private Task SaveConnectionsAsync(IEnumerable connections) return FileSys.File.WriteAllTextAsync(GetConnectionsFileName(), json); } - private void EnsureConfigPath() + private static void EnsureConfigPath() { FileSys.Directory.CreateDirectory(Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs index 51b878d..1e07467 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Connections/CliHomeMaticClientBuilder.cs @@ -24,7 +24,7 @@ public async Task BuildMultiCcuClientAsync() var credential = _ccuConnectionsStore.GetCredentials(x); _multiCcuClientFactory.AddCcu(x.Name, x.Url.Host, credential.UserName, credential.Password, - [CcuDeviceKind.HomeMatic, CcuDeviceKind.HomeMaticIp]); + CcuDeviceKind.HomeMatic, CcuDeviceKind.HomeMaticIp); }); return _multiCcuClientFactory.Build(); @@ -39,7 +39,7 @@ public IMultiCcuClient BuildMultiCcuClient() var credential = _ccuConnectionsStore.GetCredentials(x); _multiCcuClientFactory.AddCcu(x.Name, x.Url.Host, credential.UserName, credential.Password, - [CcuDeviceKind.HomeMatic, CcuDeviceKind.HomeMaticIp]); + CcuDeviceKind.HomeMatic, CcuDeviceKind.HomeMaticIp); }); return _multiCcuClientFactory.Build(); diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/SharedData/DefaultSharedData.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/SharedData/DefaultSharedData.cs index 434bbb5..d4e0afa 100644 --- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/SharedData/DefaultSharedData.cs +++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/SharedData/DefaultSharedData.cs @@ -31,7 +31,7 @@ public CliSharedData LoadCliData() } return JsonSerializer.Deserialize(FileSys.File.ReadAllText(GetCliDataFileName())) - ?? new CliSharedData(); + ?? new CliSharedData(); } public void SaveCliData(CliSharedData cliSharedData) @@ -41,6 +41,6 @@ public void SaveCliData(CliSharedData cliSharedData) public string GetPassword(string ccuHost) { - return _console.Prompt(new TextPrompt("Password: ") { IsSecret = true }); + return _console.Prompt(new TextPrompt("Password: ") { IsSecret = true }); } } From 33cc6d3877076206936528e109cb1329fa4c148e Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:07:54 +0200 Subject: [PATCH 15/15] chore(tools): add `.editorconfig` with `configure_await_analysis_mode` disabled for C# and VB files --- source/Tools/.editorconfig | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 source/Tools/.editorconfig diff --git a/source/Tools/.editorconfig b/source/Tools/.editorconfig new file mode 100644 index 0000000..c1fdbb9 --- /dev/null +++ b/source/Tools/.editorconfig @@ -0,0 +1,2 @@ +[*.{cs,vb}] +configure_await_analysis_mode = disabled