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/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
diff --git a/HomeMatic.sln b/HomeMatic.sln
index 09122e0..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}"
@@ -74,6 +75,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/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/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/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/CcuRpcPorts.cs b/source/CreativeCoders.HomeMatic.Core/CcuRpcPorts.cs
deleted file mode 100644
index 3266126..0000000
--- a/source/CreativeCoders.HomeMatic.Core/CcuRpcPorts.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Core;
-
-[PublicAPI]
-public static class CcuRpcPorts
-{
- public const int CoupledDevices = 9292;
-
- public const int HomeMatic = 2001;
-
- public const int HomeMaticIp = 2010;
-
- public const int HomeMaticWired = 2000;
-
- public static int ToPort(this HomeMaticDeviceSystems deviceSystems)
- {
- return deviceSystems switch
- {
- HomeMaticDeviceSystems.HomeMatic => HomeMatic,
- HomeMaticDeviceSystems.HomeMaticIp => HomeMaticIp,
- HomeMaticDeviceSystems.HomeMaticWired => HomeMaticWired,
- HomeMaticDeviceSystems.CoupledDevice => CoupledDevices,
- _ => 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.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..57f6509 100644
--- a/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs
+++ b/source/CreativeCoders.HomeMatic.Core/Devices/CcuParameterDescription.cs
@@ -1,29 +1,76 @@
using System.Collections.Generic;
-using CreativeCoders.HomeMatic.Core.Parameters;
+using CreativeCoders.HomeMatic.XmlRpc.Parameters;
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/ChannelDirection.cs b/source/CreativeCoders.HomeMatic.Core/Devices/ChannelDirection.cs
deleted file mode 100644
index 29fa9dc..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Devices/ChannelDirection.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Core.Devices;
-
-[PublicAPI]
-public enum ChannelDirection
-{
- None = 0,
- Sender = 1,
- Receiver = 2
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Devices/DeviceFirmwareUpdateState.cs b/source/CreativeCoders.HomeMatic.Core/Devices/DeviceFirmwareUpdateState.cs
deleted file mode 100644
index 4e8f63c..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Devices/DeviceFirmwareUpdateState.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Core.Devices;
-
-[PublicAPI]
-public enum DeviceFirmwareUpdateState
-{
- None,
- UpToDate,
- NewFirmwareAvailable,
- DeliverFirmwareImage,
- ReadyForUpdate,
- PerformingUpdate
-}
\ No newline at end of file
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..28d476e 100644
--- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs
+++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceBaseData.cs
@@ -1,18 +1,52 @@
+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
{
+ ///
+ /// 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..98c95dd 100644
--- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs
+++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannel.cs
@@ -1,5 +1,6 @@
namespace CreativeCoders.HomeMatic.Core.Devices;
-public interface ICcuDeviceChannel : ICcuDeviceBase, ICcuDeviceChannelData
-{
-}
+///
+/// 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 d6788cf..3ccdfb8 100644
--- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs
+++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceChannelData.cs
@@ -1,10 +1,29 @@
+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
{
+ ///
+ /// 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 50ac0fd..c87e201 100644
--- a/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs
+++ b/source/CreativeCoders.HomeMatic.Core/Devices/ICcuDeviceData.cs
@@ -1,20 +1,54 @@
-using CreativeCoders.HomeMatic.Core.Parameters;
+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
{
+ ///
+ /// Gets the human-readable name of the device.
+ ///
+ /// The device name.
string Name { get; }
- RxMode RxMode { get; }
+ ///
+ /// Gets the reception mode flags for the device.
+ ///
+ /// A bitwise combination of the enumeration values that specifies the reception mode.
+ RxModes 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/Exceptions/CcuXmlRpcException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/CcuXmlRpcException.cs
deleted file mode 100644
index 53020f2..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/CcuXmlRpcException.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-[PublicAPI]
-public abstract class CcuXmlRpcException : HomeMaticException
-{
- protected CcuXmlRpcException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceAddressExpectedException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceAddressExpectedException.cs
deleted file mode 100644
index 055b859..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceAddressExpectedException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class DeviceAddressExpectedException : CcuXmlRpcException
-{
- public DeviceAddressExpectedException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceOutOfReachException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceOutOfReachException.cs
deleted file mode 100644
index 20ebf16..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/DeviceOutOfReachException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class DeviceOutOfReachException : CcuXmlRpcException
-{
- public DeviceOutOfReachException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/GeneralXmlRpcException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/GeneralXmlRpcException.cs
deleted file mode 100644
index 82fe598..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/GeneralXmlRpcException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class GeneralException : CcuXmlRpcException
-{
- public GeneralException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/HomeMaticException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/HomeMaticException.cs
deleted file mode 100644
index dbeb19a..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/HomeMaticException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public abstract class HomeMaticException : Exception
-{
- protected HomeMaticException(string message, Exception innerException) : base(message, innerException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/InterfaceUpdateFailedException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/InterfaceUpdateFailedException.cs
deleted file mode 100644
index 565677d..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/InterfaceUpdateFailedException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class InterfaceUpdateFailedException : CcuXmlRpcException
-{
- public InterfaceUpdateFailedException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/NotEnoughDutyCycleException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/NotEnoughDutyCycleException.cs
deleted file mode 100644
index 42d7ca5..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/NotEnoughDutyCycleException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class NotEnoughDutyCycleException : CcuXmlRpcException
-{
- public NotEnoughDutyCycleException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/OperationNotSupportedException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/OperationNotSupportedException.cs
deleted file mode 100644
index e56566c..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/OperationNotSupportedException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class OperationNotSupportedException : CcuXmlRpcException
-{
- public OperationNotSupportedException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/TransmissionOutstandingException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/TransmissionOutstandingException.cs
deleted file mode 100644
index d9a2619..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/TransmissionOutstandingException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class TransmissionOutstandingException : CcuXmlRpcException
-{
- public TransmissionOutstandingException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownDeviceOrChannelException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownDeviceOrChannelException.cs
deleted file mode 100644
index 2a92922..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownDeviceOrChannelException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class UnknownDeviceOrChannelException : CcuXmlRpcException
-{
- public UnknownDeviceOrChannelException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParamSetException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParamSetException.cs
deleted file mode 100644
index dc57730..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParamSetException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class UnknownParamSetException : CcuXmlRpcException
-{
- public UnknownParamSetException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParameterOrValueException.cs b/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParameterOrValueException.cs
deleted file mode 100644
index 200beb8..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Exceptions/UnknownParameterOrValueException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core.Exceptions;
-
-public class UnknownParameterOrValueException : CcuXmlRpcException
-{
- public UnknownParameterOrValueException(string message, Exception faultException) : base(message, faultException)
- {
- }
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/HomeMaticDeviceSystems.cs b/source/CreativeCoders.HomeMatic.Core/HomeMaticDeviceSystems.cs
deleted file mode 100644
index 0d6dbff..0000000
--- a/source/CreativeCoders.HomeMatic.Core/HomeMaticDeviceSystems.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-
-namespace CreativeCoders.HomeMatic.Core;
-
-[Flags]
-public enum HomeMaticDeviceSystems
-{
- HomeMatic = 1,
- HomeMaticIp = 2,
- HomeMaticWired = 4,
- CoupledDevice = 8
-}
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/ICcuRoutingTable.cs b/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs
new file mode 100644
index 0000000..aa298dd
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.Core/ICcuRoutingTable.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+
+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.
+///
+[PublicAPI]
+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.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..f211367 100644
--- a/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs
+++ b/source/CreativeCoders.HomeMatic.Core/IMultiCcuClient.cs
@@ -1,16 +1,39 @@
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
{
+ ///
+ /// 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..5ef40a0 100644
--- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs
+++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParamSetKey.cs
@@ -2,19 +2,42 @@
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.
+ ///
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";
- public static readonly string[] ParamSetKeys = {Master, Values, Link, 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/ParameterDataType.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterDataType.cs
deleted file mode 100644
index 84d5ea0..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterDataType.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Core.Parameters;
-
-[PublicAPI]
-public enum ParameterDataType
-{
- Unknown,
- Integer,
- Bool,
- Float,
- Action,
- Enum,
- String
-}
\ No newline at end of file
diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs
deleted file mode 100644
index c67bd23..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterFlags.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Core.Parameters;
-
-[PublicAPI]
-[Flags]
-public enum ParameterFlags
-{
- None = 0,
- Visible = 1,
- Internal = 2,
- Transform = 4,
- Service = 8,
- 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.Core/Parameters/ParameterUiAttributes.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterUiAttributes.cs
new file mode 100644
index 0000000..556a8ef
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.Core/Parameters/ParameterUiAttributes.cs
@@ -0,0 +1,45 @@
+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 ParameterUiAttributes
+{
+ ///
+ /// 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
+}
diff --git a/source/CreativeCoders.HomeMatic.Core/Parameters/RxMode.cs b/source/CreativeCoders.HomeMatic.Core/Parameters/RxMode.cs
deleted file mode 100644
index 1a6ad41..0000000
--- a/source/CreativeCoders.HomeMatic.Core/Parameters/RxMode.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Core.Parameters;
-
-[PublicAPI]
-[Flags]
-public enum RxMode
-{
- None = 0,
- Always = 1,
- Burst = 2,
- Config = 4,
- WakeUp = 8,
- LazyConfig = 16
-}
\ No newline at end of file
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 @@
-
+
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.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/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
new file mode 100644
index 0000000..435299f
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/CcuRpcPorts.cs
@@ -0,0 +1,49 @@
+using System;
+using JetBrains.Annotations;
+
+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;
+
+ ///
+ /// 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 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/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 e29015c..167c2f6 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;
@@ -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/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..af0aa96 100644
--- a/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/DeviceDescription.cs
@@ -1,7 +1,6 @@
-using System;
-using System.Collections.Generic;
-using CreativeCoders.HomeMatic.Core.Devices;
-using CreativeCoders.HomeMatic.Core.Parameters;
+using System.Collections.Generic;
+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;
@@ -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,14 +98,14 @@ 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.
///
- /// A bitwise combination of values.
- [XmlRpcStructMember("RX_MODE", DefaultValue = RxMode.None, Converter = typeof(FlagsMemberValueConverter))]
- public RxMode RxMode { get; set; }
+ /// A bitwise combination of values.
+ [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 +148,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; }
///
@@ -165,8 +165,9 @@ 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))]
+ /// One of the values.
+ [XmlRpcStructMember("DIRECTION", DefaultValue = ChannelDirection.None,
+ Converter = typeof(EnumMemberValueConverter))]
public ChannelDirection ChannelDirection { get; set; }
///
@@ -175,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).
@@ -199,4 +200,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/Devices/ChannelDirection.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs
new file mode 100644
index 0000000..8070634
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/ChannelDirection.cs
@@ -0,0 +1,25 @@
+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
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs
new file mode 100644
index 0000000..2a3f852
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Devices/DeviceFirmwareUpdateState.cs
@@ -0,0 +1,40 @@
+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
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs
new file mode 100644
index 0000000..71608bc
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/CcuXmlRpcException.cs
@@ -0,0 +1,20 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs
new file mode 100644
index 0000000..2fc0571
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceAddressExpectedException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs
new file mode 100644
index 0000000..159cfe9
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/DeviceOutOfReachException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs
new file mode 100644
index 0000000..aeb44ca
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/GeneralXmlRpcException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs
new file mode 100644
index 0000000..ce07018
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/HomeMaticException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs
new file mode 100644
index 0000000..8803ac0
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/InterfaceUpdateFailedException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs
new file mode 100644
index 0000000..09c94a0
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/NotEnoughDutyCycleException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs
new file mode 100644
index 0000000..42733ac
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/OperationNotSupportedException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs
new file mode 100644
index 0000000..e336c67
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/TransmissionOutstandingException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs
new file mode 100644
index 0000000..43502a7
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownDeviceOrChannelException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs
new file mode 100644
index 0000000..a8917d4
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParamSetException.cs
@@ -0,0 +1,20 @@
+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
+{
+ ///
+ /// 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)
+ {
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs
new file mode 100644
index 0000000..fc67396
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Exceptions/UnknownParameterOrValueException.cs
@@ -0,0 +1,18 @@
+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)
+ {
+ }
+}
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.XmlRpc/Parameters/ParameterDataType.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs
new file mode 100644
index 0000000..367be27
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/ParameterDataType.cs
@@ -0,0 +1,45 @@
+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
+}
diff --git a/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxModes.cs b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxModes.cs
new file mode 100644
index 0000000..c6d2d06
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic.XmlRpc/Parameters/RxModes.cs
@@ -0,0 +1,45 @@
+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 RxModes
+{
+ ///
+ /// 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
+}
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.XmlRpc/XmlRpcApiAddress.cs b/source/CreativeCoders.HomeMatic.XmlRpc/XmlRpcApiAddress.cs
index 1deb6ce..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.Core;
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..ba478b1 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();
@@ -31,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()
@@ -48,15 +52,16 @@ 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();
}
+ ///
public async Task GetDeviceAsync(string address)
{
return (await GetDevicesAsync().ConfigureAwait(false))
@@ -64,6 +69,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 +82,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 dd2645b..276873c 100644
--- a/source/CreativeCoders.HomeMatic/CcuClientFactory.cs
+++ b/source/CreativeCoders.HomeMatic/CcuClientFactory.cs
@@ -2,16 +2,24 @@
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;
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)
{
@@ -33,8 +41,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 +51,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/CcuDevice.cs b/source/CreativeCoders.HomeMatic/CcuDevice.cs
index e1024f4..b38bb57 100644
--- a/source/CreativeCoders.HomeMatic/CcuDevice.cs
+++ b/source/CreativeCoders.HomeMatic/CcuDevice.cs
@@ -1,27 +1,46 @@
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;
+///
+/// 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 RxMode RxMode { get; init; }
+ ///
+ 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..21e2dcd 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,12 +43,13 @@ public async Task> GetParamSetValuesAsync(string para
});
}
+ ///
public async Task GetParamSetDescriptionsAsync(string paramSetKey)
{
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 8ebe277..1d49622 100644
--- a/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs
+++ b/source/CreativeCoders.HomeMatic/CcuDeviceBuilder.cs
@@ -1,126 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
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
-
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;
-// }
-// }
-
+///
+/// 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
+ [SuppressMessage("csharpsquid", "S3459",
+ Justification = "Fields are set via WithField method and not directly assigned to.")]
private CcuDeviceUri? _uri;
private DeviceDescription? _deviceDescription;
@@ -129,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;
@@ -139,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;
@@ -146,6 +59,12 @@ public CcuDeviceBuilder FromDeviceDescription(DeviceDescription deviceDescriptio
return this;
}
+ ///
+ ///
+ /// Builds the CcuDevice from the previously configured values.
+ ///
+ /// 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)
@@ -160,7 +79,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,
@@ -168,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;
@@ -208,6 +127,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;
@@ -216,9 +141,23 @@ 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)
{
var member = (MemberExpression)property.Body;
@@ -236,5 +175,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 f2beb9a..364486f 100644
--- a/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs
+++ b/source/CreativeCoders.HomeMatic/CcuDeviceChannel.cs
@@ -1,13 +1,21 @@
using CreativeCoders.HomeMatic.Core.Devices;
using CreativeCoders.HomeMatic.XmlRpc.Client;
+using CreativeCoders.HomeMatic.XmlRpc.Devices;
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/CcuRoutingTable.cs b/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs
new file mode 100644
index 0000000..717526a
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic/CcuRoutingTable.cs
@@ -0,0 +1,55 @@
+using System.Collections.Concurrent;
+using CreativeCoders.Core;
+using CreativeCoders.HomeMatic.Core;
+
+namespace CreativeCoders.HomeMatic;
+
+///
+///
+/// Default thread-safe implementation of ICcuRoutingTable backed by a
+/// ConcurrentDictionary{TKey,TValue}.
+///
+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)
+ {
+ foreach (var entry in Ensure.NotNull(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/CompleteCcuDevice.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs
index a3b80ab..544faad 100644
--- a/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs
+++ b/source/CreativeCoders.HomeMatic/CompleteCcuDevice.cs
@@ -2,11 +2,18 @@
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..06b22a8 100644
--- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs
+++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceBuilder.cs
@@ -4,8 +4,14 @@
namespace CreativeCoders.HomeMatic;
+///
+///
+/// Default implementation of ICompleteCcuDeviceBuilder that augments an ICcuDevice
+/// 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);
@@ -20,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();
@@ -38,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();
@@ -54,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/CompleteCcuDeviceChannel.cs b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs
index fd277fd..cb5b2c6 100644
--- a/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs
+++ b/source/CreativeCoders.HomeMatic/CompleteCcuDeviceChannel.cs
@@ -2,9 +2,15 @@
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/DeviceExportOptions.cs b/source/CreativeCoders.HomeMatic/Exporting/DeviceExportOptions.cs
index 062a780..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
{
///
@@ -8,18 +11,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..881970c 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
@@ -49,7 +59,7 @@ private static ChannelExportData BuildChannelExportData(
};
}
- private static IEnumerable BuildParamSetExportData(
+ private static ParamSetExportData[] BuildParamSetExportData(
IEnumerable paramSetValues,
DeviceExportOptions? options)
{
@@ -58,14 +68,16 @@ 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();
+ .ToArray();
}
private static string Serialize(T data, DeviceExportOptions? options)
diff --git a/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs b/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs
index ec92f19..24440f4 100644
--- a/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs
+++ b/source/CreativeCoders.HomeMatic/Exporting/IDeviceExporter.cs
@@ -1,12 +1,36 @@
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
{
+ ///
+ /// 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 dc4014f..18a11c9 100644
--- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
+++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
@@ -1,13 +1,30 @@
using CreativeCoders.HomeMatic.Core;
+using CreativeCoders.HomeMatic.Exporting;
using CreativeCoders.HomeMatic.JsonRpc;
using CreativeCoders.HomeMatic.XmlRpc;
+using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace CreativeCoders.HomeMatic;
+///
+/// Provides extension methods for registering the HomeMatic services on an .
+///
+[PublicAPI]
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();
@@ -16,6 +33,7 @@ public static IServiceCollection AddHomeMatic(this IServiceCollection services)
services.TryAddTransient();
services.TryAddTransient();
services.TryAddTransient();
+ services.TryAddSingleton();
return services;
}
diff --git a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs
index d7334c8..48de712 100644
--- a/source/CreativeCoders.HomeMatic/MultiCcuClient.cs
+++ b/source/CreativeCoders.HomeMatic/MultiCcuClient.cs
@@ -1,56 +1,133 @@
+using CreativeCoders.Core;
using CreativeCoders.HomeMatic.Core;
using CreativeCoders.HomeMatic.Core.Devices;
namespace CreativeCoders.HomeMatic;
-public class MultiCcuClient(
- IEnumerable ccuClients) : IMultiCcuClient
+///
+///
+/// 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 ICcuClient.
+///
+/// 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
{
- public Task> GetDevicesAsync()
+ private readonly IReadOnlyList _ccuClients = Ensure.NotNull(ccuClients).ToList();
+
+ private readonly ICcuRoutingTable _routingTable = Ensure.NotNull(routingTable);
+
+ ///
+ public async Task> GetDevicesAsync()
{
- return GetDataFromClientsAsync(x => x.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 async Task GetDeviceAsync(string address)
+ ///
+ public Task GetDeviceAsync(string address)
{
- return (await GetDevicesAsync().ConfigureAwait(false)).FirstOrDefault(x => x.Uri.Address == address) ??
- throw new KeyNotFoundException($"Device with address '{address}' not found.");
+ Ensure.IsNotNullOrWhitespace(address);
+
+ return InvokeWithRoutingAsync(address, (client, deviceAddress) => client.GetDeviceAsync(deviceAddress));
}
- public Task> GetCompleteDevicesAsync()
+ ///
+ public async Task> GetCompleteDevicesAsync()
{
- return GetDataFromClientsAsync(x => x.GetCompleteDevicesAsync());
+ 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 async Task GetCompleteDeviceAsync(string address)
+ ///
+ public Task GetCompleteDeviceAsync(string address)
{
- foreach (var ccuClient in ccuClients)
+ Ensure.IsNotNullOrWhitespace(address);
+
+ return InvokeWithRoutingAsync(address,
+ (client, deviceAddress) => client.GetCompleteDeviceAsync(deviceAddress));
+ }
+
+ // 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)
+ {
+ 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);
+ }
+ }
+
+ foreach (var ccuClient in _ccuClients.Where(ccuClient => !ReferenceEquals(ccuClient, cachedClient)))
+ {
+ try
+ {
+ var result = await func(ccuClient, address).ConfigureAwait(false);
+
+ _routingTable.Register(deviceAddress, ccuClient);
- return completeDevice;
+ return result;
}
catch (KeyNotFoundException)
{
+ // Device not found on this client; try the next one.
}
}
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 5075b7f..45e4697 100644
--- a/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs
+++ b/source/CreativeCoders.HomeMatic/MultiCcuClientFactory.cs
@@ -1,11 +1,19 @@
using CreativeCoders.HomeMatic.Core;
+using CreativeCoders.HomeMatic.XmlRpc;
namespace CreativeCoders.HomeMatic;
+///
+///
+/// Default implementation of IMultiCcuClientFactory that collects CCU configurations and
+/// builds an IMultiCcuClient backed by a CcuRoutingTable.
+///
+/// The factory used to create the per-CCU ICcuClient 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)
{
@@ -16,8 +24,9 @@ public IMultiCcuClientFactory AddCcu(string ccuName, string host, string userNam
return this;
}
+ ///
public IMultiCcuClient Build()
{
- return new MultiCcuClient(_ccuClients);
+ return new MultiCcuClient(_ccuClients, new CcuRoutingTable());
}
}
diff --git a/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs b/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs
index 2434630..467378e 100644
--- a/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs
+++ b/source/CreativeCoders.HomeMatic/XmlRpcApiConnection.cs
@@ -1,13 +1,32 @@
using CreativeCoders.Core;
+using CreativeCoders.HomeMatic.XmlRpc;
using CreativeCoders.HomeMatic.XmlRpc.Client;
namespace CreativeCoders.HomeMatic;
-public class XmlRpcApiConnection(XmlRpcEndpoint endpoint, IHomeMaticXmlRpcApi api)
+///
+/// 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)
{
- public XmlRpcEndpoint Endpoint { get; } = Ensure.NotNull(endpoint);
+ ///
+ /// 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);
}
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/.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
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..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
@@ -1,28 +1,22 @@
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;
-public abstract class CliBaseCommand
+public abstract class CliBaseCommand(IHomeMaticXmlRpcApiBuilder apiBuilder, ISharedData sharedData)
{
- private readonly IHomeMaticXmlRpcApiBuilder _apiBuilder;
+ private readonly IHomeMaticXmlRpcApiBuilder _apiBuilder = Ensure.NotNull(apiBuilder);
- protected CliBaseCommand(IHomeMaticXmlRpcApiBuilder apiBuilder, ISharedData sharedData)
- {
- _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();
}
- 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/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.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 d95d103..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
@@ -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;
@@ -23,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();
@@ -38,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 });
}
}
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/CcuClientFactoryTests.cs b/tests/CreativeCoders.HomeMatic.Tests/CcuClientFactoryTests.cs
index 5d3f21f..a62f219 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;
@@ -42,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 7cffc3c..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"
}
];
@@ -256,17 +256,287 @@ 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,
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
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 64089b4..110b537 100644
--- a/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs
+++ b/tests/CreativeCoders.HomeMatic.Tests/CcuDeviceBuilderTests.cs
@@ -1,8 +1,8 @@
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;
@@ -21,7 +21,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",
@@ -56,7 +56,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");
@@ -107,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()
{
@@ -132,7 +240,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);
@@ -141,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/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/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/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