From 64a4c85378a41feb2b8649a0d28f0849eea98d2f Mon Sep 17 00:00:00 2001
From: darthsharp <48331467+darthsharp@users.noreply.github.com>
Date: Sun, 26 Apr 2026 10:40:50 +0200
Subject: [PATCH 01/16] feat(cli): add CCU command group and enhance CLI with
CCU backup support
- Introduced `CcuCommandGroup` to group CCU-related commands in the CLI.
- Enhanced `ShowDeviceDetailsCommand` with `CliCommand` attribute for better integration.
- Added `ICcuBackupServiceBuilder` to support CCU backup handling.
- Updated project dependencies and service registrations to include new functionality.
---
.../CreativeCoders.HomeMatic.csproj | 4 ++++
.../HomeMaticServiceCollectionExtensions.cs | 2 ++
.../Ccu/CcuCommandGroup.cs | 17 +++++++++++++++++
.../ShowDetails/ShowDeviceDetailsCommand.cs | 1 +
4 files changed, 24 insertions(+)
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs
diff --git a/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj b/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj
index aa6a69c..302f4d3 100644
--- a/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj
+++ b/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj
@@ -5,6 +5,10 @@
enable
+
+
+
+
diff --git a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
index 18a11c9..d71b4a1 100644
--- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
+++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
@@ -1,3 +1,4 @@
+using CreativeCoders.HomeMatic.Backup;
using CreativeCoders.HomeMatic.Core;
using CreativeCoders.HomeMatic.Exporting;
using CreativeCoders.HomeMatic.JsonRpc;
@@ -34,6 +35,7 @@ public static IServiceCollection AddHomeMatic(this IServiceCollection services)
services.TryAddTransient();
services.TryAddTransient();
services.TryAddSingleton();
+ services.TryAddTransient();
return services;
}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs
new file mode 100644
index 0000000..2e95906
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs
@@ -0,0 +1,17 @@
+using CreativeCoders.Cli.Core;
+using CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu;
+
+[assembly: CliCommandGroup([CcuCommandGroup.Name], "Commands for CCU operations")]
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu;
+
+///
+/// Defines the command group for CCU operations.
+///
+public static class CcuCommandGroup
+{
+ ///
+ /// The name of the CCU command group.
+ ///
+ public const string Name = "ccu";
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/ShowDetails/ShowDeviceDetailsCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/ShowDetails/ShowDeviceDetailsCommand.cs
index d9abfa6..554fff0 100644
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/ShowDetails/ShowDeviceDetailsCommand.cs
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Device/ShowDetails/ShowDeviceDetailsCommand.cs
@@ -10,6 +10,7 @@
namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Device.ShowDetails;
[UsedImplicitly]
+[CliCommand([DeviceCommandGroup.Name, "details"], Description = "Show details for a device")]
public class ShowDeviceDetailsCommand(IAnsiConsole console, IMultiCcuClient multiCcuClient)
: ICliCommand
{
From 99926f452b48f0c55ed387910278aca361ded44a Mon Sep 17 00:00:00 2001
From: darthsharp <48331467+darthsharp@users.noreply.github.com>
Date: Sun, 26 Apr 2026 10:54:46 +0200
Subject: [PATCH 02/16] feat(homeMatic): add HTTP client for CCU backup service
- Registered a named `HttpClient` ("CcuBackup") in `HomeMaticServiceCollectionExtensions`.
- Enables support for CCU backup handling via `ICcuBackupServiceBuilder`.
---
.../HomeMaticServiceCollectionExtensions.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
index d71b4a1..1c8e5ad 100644
--- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
+++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
@@ -35,6 +35,7 @@ public static IServiceCollection AddHomeMatic(this IServiceCollection services)
services.TryAddTransient();
services.TryAddTransient();
services.TryAddSingleton();
+ services.AddHttpClient("CcuBackup");
services.TryAddTransient();
return services;
From d161f220b006fd4c883ac5ad8f8a9993e9716dd3 Mon Sep 17 00:00:00 2001
From: darthsharp <48331467+darthsharp@users.noreply.github.com>
Date: Sun, 26 Apr 2026 10:59:03 +0200
Subject: [PATCH 03/16] feat(homeMatic, cli): add CCU backup service and CLI
command
- Implemented `CcuBackupService` to handle HomeMatic CCU system backups via JSON-RPC and HTTP.
- Added `CcuBackupServiceBuilder` for flexible service configuration based on host and credentials.
- Introduced CLI support with the `CreateBackupCommand` and `CreateBackupOptions` for managing CCU backups directly from the command line.
- Extended `HomeMaticServiceCollectionExtensions` to register CCU backup services.
---
.gitignore | 1 -
.../Backup/CcuBackupService.cs | 161 ++++++++++++++++++
.../Backup/CcuBackupServiceBuilder.cs | 58 +++++++
.../Backup/ICcuBackupService.cs | 26 +++
.../Backup/ICcuBackupServiceBuilder.cs | 31 ++++
.../HomeMaticServiceCollectionExtensions.cs | 4 +-
.../Ccu/Backup/CreateBackupCommand.cs | 62 +++++++
.../Ccu/Backup/CreateBackupOptions.cs | 23 +++
8 files changed, 364 insertions(+), 2 deletions(-)
create mode 100644 source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
create mode 100644 source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs
create mode 100644 source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs
create mode 100644 source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs
diff --git a/.gitignore b/.gitignore
index 19b8cbb..7d64e26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -231,7 +231,6 @@ Generated_Code/
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
-Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
diff --git a/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs b/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
new file mode 100644
index 0000000..6d8828b
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
@@ -0,0 +1,161 @@
+using System.Net;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using CreativeCoders.Core;
+
+namespace CreativeCoders.HomeMatic.Backup;
+
+///
+/// Creates system backups of a HomeMatic CCU by authenticating via JSON-RPC and downloading
+/// the backup archive over HTTP.
+///
+public class CcuBackupService : ICcuBackupService
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+
+ private readonly HttpClient _httpClient;
+ private readonly Uri _baseUrl;
+ private readonly NetworkCredential _credential;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The HTTP client used for communication with the CCU.
+ /// The base URL of the CCU (e.g. http://192.168.1.100).
+ /// The credentials used to authenticate against the CCU.
+ public CcuBackupService(HttpClient httpClient, Uri baseUrl, NetworkCredential credential)
+ {
+ _httpClient = Ensure.NotNull(httpClient);
+ _baseUrl = Ensure.NotNull(baseUrl);
+ _credential = Ensure.NotNull(credential);
+ }
+
+ ///
+ public async Task CreateBackupAsync(CancellationToken cancellationToken = default)
+ {
+ var sessionId = await LoginAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ var backupUrl = new Uri(_baseUrl, $"/config/cp_security.cgi?sid={sessionId}&action=create_backup");
+
+ using var backupRequest = new HttpRequestMessage(HttpMethod.Get, backupUrl);
+ backupRequest.Headers.Add("Cookie", $"SID={sessionId}");
+
+ var response = await _httpClient.SendAsync(backupRequest, HttpCompletionOption.ResponseHeadersRead,
+ cancellationToken).ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+ var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
+
+ if (contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
+ {
+ var errorBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ throw new InvalidOperationException($"CCU backup failed: {errorBody.Trim()}");
+ }
+
+ var memoryStream = new MemoryStream();
+
+ await response.Content.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
+
+ memoryStream.Position = 0;
+
+ return memoryStream;
+ }
+ finally
+ {
+ await LogoutAsync(sessionId, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ public async Task SaveBackupAsync(string outputFilePath, CancellationToken cancellationToken = default)
+ {
+ Ensure.IsNotNullOrWhitespace(outputFilePath);
+
+ await using var backupStream = await CreateBackupAsync(cancellationToken).ConfigureAwait(false);
+ await using var fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write, FileShare.None,
+ bufferSize: 81920, useAsync: true);
+
+ await backupStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task LoginAsync(CancellationToken cancellationToken)
+ {
+ var request = new JsonRpcRequest
+ {
+ Method = "Session.login",
+ Params = new Dictionary
+ {
+ ["username"] = _credential.UserName,
+ ["password"] = _credential.Password
+ }
+ };
+
+ var apiUrl = new Uri(_baseUrl, "/api/homematic.cgi");
+ var content = new StringContent(JsonSerializer.Serialize(request, JsonOptions),
+ System.Text.Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync(apiUrl, content, cancellationToken).ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+
+ var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var loginResponse = await JsonSerializer.DeserializeAsync(responseStream, JsonOptions,
+ cancellationToken).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(loginResponse?.Result))
+ {
+ throw new InvalidOperationException("CCU login failed: no session ID received.");
+ }
+
+ return loginResponse.Result;
+ }
+
+ private async Task LogoutAsync(string sessionId, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var request = new JsonRpcRequest
+ {
+ Method = "Session.logout",
+ Params = new Dictionary
+ {
+ ["_session_id_"] = sessionId
+ }
+ };
+
+ var apiUrl = new Uri(_baseUrl, "/api/homematic.cgi");
+ var content = new StringContent(JsonSerializer.Serialize(request, JsonOptions),
+ System.Text.Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync(apiUrl, content, cancellationToken).ConfigureAwait(false);
+
+ response.EnsureSuccessStatusCode();
+ }
+ catch
+ {
+ // Best-effort logout — do not let logout failures mask the original operation result.
+ }
+ }
+
+ private record JsonRpcRequest
+ {
+ [JsonPropertyName("method")]
+ public required string Method { get; init; }
+
+ [JsonPropertyName("params")]
+ public required Dictionary Params { get; init; }
+ }
+
+ private record JsonRpcResponse
+ {
+ [JsonPropertyName("result")]
+ public string? Result { get; init; }
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs b/source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs
new file mode 100644
index 0000000..3fdb895
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs
@@ -0,0 +1,58 @@
+using System.Net;
+using CreativeCoders.Core;
+
+namespace CreativeCoders.HomeMatic.Backup;
+
+///
+/// Builds instances configured for a specific CCU host and credentials.
+///
+public class CcuBackupServiceBuilder(IHttpClientFactory httpClientFactory) : ICcuBackupServiceBuilder
+{
+ private readonly IHttpClientFactory _httpClientFactory = Ensure.NotNull(httpClientFactory);
+
+ private string? _host;
+
+ private NetworkCredential? _credential;
+
+ ///
+ public ICcuBackupServiceBuilder ForHost(string host)
+ {
+ Ensure.IsNotNullOrWhitespace(host);
+
+ _host = host;
+
+ return this;
+ }
+
+ ///
+ public ICcuBackupServiceBuilder WithCredentials(NetworkCredential credential)
+ {
+ _credential = Ensure.NotNull(credential);
+
+ return this;
+ }
+
+ ///
+ public ICcuBackupService Build()
+ {
+ if (_host is null)
+ {
+ throw new InvalidOperationException("No host specified.");
+ }
+
+ if (_credential is null)
+ {
+ throw new InvalidOperationException("No credentials specified.");
+ }
+
+ var baseUrl = new UriBuilder
+ {
+ Scheme = "http",
+ Host = _host
+ }.Uri;
+
+ var httpClient = _httpClientFactory.CreateClient("CcuBackup");
+
+ return new CcuBackupService(httpClient, baseUrl, _credential);
+ }
+}
diff --git a/source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs b/source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs
new file mode 100644
index 0000000..14fe6f4
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs
@@ -0,0 +1,26 @@
+using JetBrains.Annotations;
+
+namespace CreativeCoders.HomeMatic.Backup;
+
+///
+/// Creates system backups of a HomeMatic CCU, equivalent to the backup function
+/// available in the CCU Web UI.
+///
+[PublicAPI]
+public interface ICcuBackupService
+{
+ ///
+ /// Asynchronously creates a system backup of the CCU and returns it as a stream.
+ ///
+ /// A token to cancel the operation.
+ /// A task that yields a containing the backup archive (.tar.gz). The caller is responsible for disposing the stream.
+ Task CreateBackupAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Asynchronously creates a system backup of the CCU and saves it to the specified file path.
+ ///
+ /// The file path where the backup archive (.tar.gz) will be saved.
+ /// A token to cancel the operation.
+ /// A task representing the asynchronous operation.
+ Task SaveBackupAsync(string outputFilePath, CancellationToken cancellationToken = default);
+}
diff --git a/source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs b/source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs
new file mode 100644
index 0000000..34cce1c
--- /dev/null
+++ b/source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs
@@ -0,0 +1,31 @@
+using System.Net;
+using JetBrains.Annotations;
+
+namespace CreativeCoders.HomeMatic.Backup;
+
+///
+/// Builds instances configured for a specific CCU.
+///
+[PublicAPI]
+public interface ICcuBackupServiceBuilder
+{
+ ///
+ /// Sets the host name or IP address of the CCU to back up.
+ ///
+ /// The host name or IP address of the CCU.
+ /// The current builder instance for method chaining.
+ ICcuBackupServiceBuilder ForHost(string host);
+
+ ///
+ /// Sets the credentials used to authenticate against the CCU.
+ ///
+ /// The network credentials containing user name and password.
+ /// The current builder instance for method chaining.
+ ICcuBackupServiceBuilder WithCredentials(NetworkCredential credential);
+
+ ///
+ /// Creates a new from the current builder configuration.
+ ///
+ /// A configured instance.
+ ICcuBackupService Build();
+}
diff --git a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
index 1c8e5ad..197139f 100644
--- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
+++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
@@ -1,3 +1,4 @@
+using System.Net.Http;
using CreativeCoders.HomeMatic.Backup;
using CreativeCoders.HomeMatic.Core;
using CreativeCoders.HomeMatic.Exporting;
@@ -35,7 +36,8 @@ public static IServiceCollection AddHomeMatic(this IServiceCollection services)
services.TryAddTransient();
services.TryAddTransient();
services.TryAddSingleton();
- services.AddHttpClient("CcuBackup");
+ services.AddHttpClient("CcuBackup")
+ .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { UseCookies = false });
services.TryAddTransient();
return services;
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs
new file mode 100644
index 0000000..cdcf563
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs
@@ -0,0 +1,62 @@
+using CreativeCoders.Cli.Core;
+using CreativeCoders.Core;
+using CreativeCoders.HomeMatic.Backup;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Connections;
+using JetBrains.Annotations;
+using Spectre.Console;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu.Backup;
+
+///
+/// CLI command that creates a system backup of a CCU.
+///
+[UsedImplicitly]
+[CliCommand([CcuCommandGroup.Name, "backup"], Description = "Create a backup of a CCU")]
+public class CreateBackupCommand(
+ IAnsiConsole console,
+ ICcuConnectionsStore ccuConnectionsStore,
+ ICcuBackupServiceBuilder ccuBackupServiceBuilder)
+ : ICliCommand
+{
+ private readonly IAnsiConsole _console = Ensure.NotNull(console);
+
+ private readonly ICcuConnectionsStore _ccuConnectionsStore = Ensure.NotNull(ccuConnectionsStore);
+
+ private readonly ICcuBackupServiceBuilder _ccuBackupServiceBuilder = Ensure.NotNull(ccuBackupServiceBuilder);
+
+ ///
+ public async Task ExecuteAsync(CreateBackupOptions options)
+ {
+ var connections = await _ccuConnectionsStore.GetConnectionsAsync().ConfigureAwait(false);
+
+ var connection = connections.FirstOrDefault(
+ x => string.Equals(x.Name, options.ConnectionName, StringComparison.OrdinalIgnoreCase));
+
+ if (connection is null)
+ {
+ _console.MarkupLine($"[bold red]Connection '{options.ConnectionName}' not found[/]");
+ return -1;
+ }
+
+ var credentials = _ccuConnectionsStore.GetCredentials(connection);
+
+ var backupService = _ccuBackupServiceBuilder
+ .ForHost(connection.Url.Host)
+ .WithCredentials(credentials)
+ .Build();
+
+ var outputFilePath = options.OutputFilePath
+ ?? $"backup_{connection.Name}_{DateTime.Now:yyyyMMdd_HHmmss}.tar.gz";
+
+ await _console.Status()
+ .StartAsync($"Creating backup for '{connection.Name}'...", async _ =>
+ {
+ await backupService.SaveBackupAsync(outputFilePath).ConfigureAwait(false);
+ })
+ .ConfigureAwait(false);
+
+ _console.MarkupLine($"[bold green]Backup saved to '{outputFilePath}'[/]");
+
+ return CommandResult.Success;
+ }
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs
new file mode 100644
index 0000000..931f7cb
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs
@@ -0,0 +1,23 @@
+using CreativeCoders.SysConsole.Cli.Parsing;
+using JetBrains.Annotations;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu.Backup;
+
+///
+/// Options for the CCU backup command.
+///
+[UsedImplicitly]
+public class CreateBackupOptions
+{
+ ///
+ /// Gets or sets the name of the CCU connection to back up.
+ ///
+ [OptionValue(0, IsRequired = true)]
+ public string ConnectionName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the output file path for the backup archive. When not specified, a default file name is generated.
+ ///
+ [OptionParameter('o', "output", HelpText = "Output file path for the backup archive (.tar.gz)")]
+ public string? OutputFilePath { get; set; }
+}
From c376123b3342c276a906158c12b70969cda29bee Mon Sep 17 00:00:00 2001
From: darthsharp <48331467+darthsharp@users.noreply.github.com>
Date: Sun, 26 Apr 2026 11:44:32 +0200
Subject: [PATCH 04/16] refactor(homeMatic): simplify JSON-RPC `Params` and add
`Id` property to requests
---
.../Backup/CcuBackupService.cs | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs b/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
index 6d8828b..dc75ee8 100644
--- a/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
+++ b/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
@@ -89,12 +89,9 @@ private async Task LoginAsync(CancellationToken cancellationToken)
{
var request = new JsonRpcRequest
{
+ Id = Environment.TickCount,
Method = "Session.login",
- Params = new Dictionary
- {
- ["username"] = _credential.UserName,
- ["password"] = _credential.Password
- }
+ Params = ["username", _credential.UserName, "password", _credential.Password]
};
var apiUrl = new Uri(_baseUrl, "/api/homematic.cgi");
@@ -123,11 +120,9 @@ private async Task LogoutAsync(string sessionId, CancellationToken cancellationT
{
var request = new JsonRpcRequest
{
+ Id = Environment.TickCount,
Method = "Session.logout",
- Params = new Dictionary
- {
- ["_session_id_"] = sessionId
- }
+ Params = ["_session_id_", sessionId]
};
var apiUrl = new Uri(_baseUrl, "/api/homematic.cgi");
@@ -146,11 +141,14 @@ private async Task LogoutAsync(string sessionId, CancellationToken cancellationT
private record JsonRpcRequest
{
+ [JsonPropertyName("id")]
+ public required int Id { get; init; }
+
[JsonPropertyName("method")]
public required string Method { get; init; }
[JsonPropertyName("params")]
- public required Dictionary Params { get; init; }
+ public required object?[] Params { get; init; }
}
private record JsonRpcResponse
From bb5a4d4ed06f61b6243c036cd70d7d3905c57941 Mon Sep 17 00:00:00 2001
From: darthsharp <48331467+darthsharp@users.noreply.github.com>
Date: Sun, 26 Apr 2026 15:33:09 +0200
Subject: [PATCH 05/16] refactor(homeMatic): remove CCU backup service and CLI
command
- Deleted `CcuBackupService`, `CcuBackupServiceBuilder`, and related interfaces.
- Removed `CreateBackupCommand`, `CreateBackupOptions`, and `CcuCommandGroup`.
- Updated `HomeMaticServiceCollectionExtensions` to exclude CCU backup registration.
---
.../Backup/CcuBackupService.cs | 159 ------------------
.../Backup/CcuBackupServiceBuilder.cs | 58 -------
.../Backup/ICcuBackupService.cs | 26 ---
.../Backup/ICcuBackupServiceBuilder.cs | 31 ----
.../HomeMaticServiceCollectionExtensions.cs | 5 -
.../Ccu/Backup/CreateBackupCommand.cs | 62 -------
.../Ccu/Backup/CreateBackupOptions.cs | 23 ---
.../Ccu/CcuCommandGroup.cs | 17 --
8 files changed, 381 deletions(-)
delete mode 100644 source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
delete mode 100644 source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs
delete mode 100644 source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs
delete mode 100644 source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs
diff --git a/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs b/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
deleted file mode 100644
index dc75ee8..0000000
--- a/source/CreativeCoders.HomeMatic/Backup/CcuBackupService.cs
+++ /dev/null
@@ -1,159 +0,0 @@
-using System.Net;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using CreativeCoders.Core;
-
-namespace CreativeCoders.HomeMatic.Backup;
-
-///
-/// Creates system backups of a HomeMatic CCU by authenticating via JSON-RPC and downloading
-/// the backup archive over HTTP.
-///
-public class CcuBackupService : ICcuBackupService
-{
- private static readonly JsonSerializerOptions JsonOptions = new()
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
- };
-
- private readonly HttpClient _httpClient;
- private readonly Uri _baseUrl;
- private readonly NetworkCredential _credential;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The HTTP client used for communication with the CCU.
- /// The base URL of the CCU (e.g. http://192.168.1.100).
- /// The credentials used to authenticate against the CCU.
- public CcuBackupService(HttpClient httpClient, Uri baseUrl, NetworkCredential credential)
- {
- _httpClient = Ensure.NotNull(httpClient);
- _baseUrl = Ensure.NotNull(baseUrl);
- _credential = Ensure.NotNull(credential);
- }
-
- ///
- public async Task CreateBackupAsync(CancellationToken cancellationToken = default)
- {
- var sessionId = await LoginAsync(cancellationToken).ConfigureAwait(false);
-
- try
- {
- var backupUrl = new Uri(_baseUrl, $"/config/cp_security.cgi?sid={sessionId}&action=create_backup");
-
- using var backupRequest = new HttpRequestMessage(HttpMethod.Get, backupUrl);
- backupRequest.Headers.Add("Cookie", $"SID={sessionId}");
-
- var response = await _httpClient.SendAsync(backupRequest, HttpCompletionOption.ResponseHeadersRead,
- cancellationToken).ConfigureAwait(false);
-
- response.EnsureSuccessStatusCode();
-
- var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
-
- if (contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
- {
- var errorBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
- throw new InvalidOperationException($"CCU backup failed: {errorBody.Trim()}");
- }
-
- var memoryStream = new MemoryStream();
-
- await response.Content.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
-
- memoryStream.Position = 0;
-
- return memoryStream;
- }
- finally
- {
- await LogoutAsync(sessionId, cancellationToken).ConfigureAwait(false);
- }
- }
-
- ///
- public async Task SaveBackupAsync(string outputFilePath, CancellationToken cancellationToken = default)
- {
- Ensure.IsNotNullOrWhitespace(outputFilePath);
-
- await using var backupStream = await CreateBackupAsync(cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write, FileShare.None,
- bufferSize: 81920, useAsync: true);
-
- await backupStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
- }
-
- private async Task LoginAsync(CancellationToken cancellationToken)
- {
- var request = new JsonRpcRequest
- {
- Id = Environment.TickCount,
- Method = "Session.login",
- Params = ["username", _credential.UserName, "password", _credential.Password]
- };
-
- var apiUrl = new Uri(_baseUrl, "/api/homematic.cgi");
- var content = new StringContent(JsonSerializer.Serialize(request, JsonOptions),
- System.Text.Encoding.UTF8, "application/json");
-
- var response = await _httpClient.PostAsync(apiUrl, content, cancellationToken).ConfigureAwait(false);
-
- response.EnsureSuccessStatusCode();
-
- var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var loginResponse = await JsonSerializer.DeserializeAsync(responseStream, JsonOptions,
- cancellationToken).ConfigureAwait(false);
-
- if (string.IsNullOrEmpty(loginResponse?.Result))
- {
- throw new InvalidOperationException("CCU login failed: no session ID received.");
- }
-
- return loginResponse.Result;
- }
-
- private async Task LogoutAsync(string sessionId, CancellationToken cancellationToken)
- {
- try
- {
- var request = new JsonRpcRequest
- {
- Id = Environment.TickCount,
- Method = "Session.logout",
- Params = ["_session_id_", sessionId]
- };
-
- var apiUrl = new Uri(_baseUrl, "/api/homematic.cgi");
- var content = new StringContent(JsonSerializer.Serialize(request, JsonOptions),
- System.Text.Encoding.UTF8, "application/json");
-
- var response = await _httpClient.PostAsync(apiUrl, content, cancellationToken).ConfigureAwait(false);
-
- response.EnsureSuccessStatusCode();
- }
- catch
- {
- // Best-effort logout — do not let logout failures mask the original operation result.
- }
- }
-
- private record JsonRpcRequest
- {
- [JsonPropertyName("id")]
- public required int Id { get; init; }
-
- [JsonPropertyName("method")]
- public required string Method { get; init; }
-
- [JsonPropertyName("params")]
- public required object?[] Params { get; init; }
- }
-
- private record JsonRpcResponse
- {
- [JsonPropertyName("result")]
- public string? Result { get; init; }
- }
-}
diff --git a/source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs b/source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs
deleted file mode 100644
index 3fdb895..0000000
--- a/source/CreativeCoders.HomeMatic/Backup/CcuBackupServiceBuilder.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Net;
-using CreativeCoders.Core;
-
-namespace CreativeCoders.HomeMatic.Backup;
-
-///
-/// Builds instances configured for a specific CCU host and credentials.
-///
-public class CcuBackupServiceBuilder(IHttpClientFactory httpClientFactory) : ICcuBackupServiceBuilder
-{
- private readonly IHttpClientFactory _httpClientFactory = Ensure.NotNull(httpClientFactory);
-
- private string? _host;
-
- private NetworkCredential? _credential;
-
- ///
- public ICcuBackupServiceBuilder ForHost(string host)
- {
- Ensure.IsNotNullOrWhitespace(host);
-
- _host = host;
-
- return this;
- }
-
- ///
- public ICcuBackupServiceBuilder WithCredentials(NetworkCredential credential)
- {
- _credential = Ensure.NotNull(credential);
-
- return this;
- }
-
- ///
- public ICcuBackupService Build()
- {
- if (_host is null)
- {
- throw new InvalidOperationException("No host specified.");
- }
-
- if (_credential is null)
- {
- throw new InvalidOperationException("No credentials specified.");
- }
-
- var baseUrl = new UriBuilder
- {
- Scheme = "http",
- Host = _host
- }.Uri;
-
- var httpClient = _httpClientFactory.CreateClient("CcuBackup");
-
- return new CcuBackupService(httpClient, baseUrl, _credential);
- }
-}
diff --git a/source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs b/source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs
deleted file mode 100644
index 14fe6f4..0000000
--- a/source/CreativeCoders.HomeMatic/Backup/ICcuBackupService.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Backup;
-
-///
-/// Creates system backups of a HomeMatic CCU, equivalent to the backup function
-/// available in the CCU Web UI.
-///
-[PublicAPI]
-public interface ICcuBackupService
-{
- ///
- /// Asynchronously creates a system backup of the CCU and returns it as a stream.
- ///
- /// A token to cancel the operation.
- /// A task that yields a containing the backup archive (.tar.gz). The caller is responsible for disposing the stream.
- Task CreateBackupAsync(CancellationToken cancellationToken = default);
-
- ///
- /// Asynchronously creates a system backup of the CCU and saves it to the specified file path.
- ///
- /// The file path where the backup archive (.tar.gz) will be saved.
- /// A token to cancel the operation.
- /// A task representing the asynchronous operation.
- Task SaveBackupAsync(string outputFilePath, CancellationToken cancellationToken = default);
-}
diff --git a/source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs b/source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs
deleted file mode 100644
index 34cce1c..0000000
--- a/source/CreativeCoders.HomeMatic/Backup/ICcuBackupServiceBuilder.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System.Net;
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Backup;
-
-///
-/// Builds instances configured for a specific CCU.
-///
-[PublicAPI]
-public interface ICcuBackupServiceBuilder
-{
- ///
- /// Sets the host name or IP address of the CCU to back up.
- ///
- /// The host name or IP address of the CCU.
- /// The current builder instance for method chaining.
- ICcuBackupServiceBuilder ForHost(string host);
-
- ///
- /// Sets the credentials used to authenticate against the CCU.
- ///
- /// The network credentials containing user name and password.
- /// The current builder instance for method chaining.
- ICcuBackupServiceBuilder WithCredentials(NetworkCredential credential);
-
- ///
- /// Creates a new from the current builder configuration.
- ///
- /// A configured instance.
- ICcuBackupService Build();
-}
diff --git a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
index 197139f..18a11c9 100644
--- a/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
+++ b/source/CreativeCoders.HomeMatic/HomeMaticServiceCollectionExtensions.cs
@@ -1,5 +1,3 @@
-using System.Net.Http;
-using CreativeCoders.HomeMatic.Backup;
using CreativeCoders.HomeMatic.Core;
using CreativeCoders.HomeMatic.Exporting;
using CreativeCoders.HomeMatic.JsonRpc;
@@ -36,9 +34,6 @@ public static IServiceCollection AddHomeMatic(this IServiceCollection services)
services.TryAddTransient();
services.TryAddTransient();
services.TryAddSingleton();
- services.AddHttpClient("CcuBackup")
- .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { UseCookies = false });
- services.TryAddTransient();
return services;
}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs
deleted file mode 100644
index cdcf563..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupCommand.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using CreativeCoders.Cli.Core;
-using CreativeCoders.Core;
-using CreativeCoders.HomeMatic.Backup;
-using CreativeCoders.HomeMatic.Tools.Cli.Base.Connections;
-using JetBrains.Annotations;
-using Spectre.Console;
-
-namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu.Backup;
-
-///
-/// CLI command that creates a system backup of a CCU.
-///
-[UsedImplicitly]
-[CliCommand([CcuCommandGroup.Name, "backup"], Description = "Create a backup of a CCU")]
-public class CreateBackupCommand(
- IAnsiConsole console,
- ICcuConnectionsStore ccuConnectionsStore,
- ICcuBackupServiceBuilder ccuBackupServiceBuilder)
- : ICliCommand
-{
- private readonly IAnsiConsole _console = Ensure.NotNull(console);
-
- private readonly ICcuConnectionsStore _ccuConnectionsStore = Ensure.NotNull(ccuConnectionsStore);
-
- private readonly ICcuBackupServiceBuilder _ccuBackupServiceBuilder = Ensure.NotNull(ccuBackupServiceBuilder);
-
- ///
- public async Task ExecuteAsync(CreateBackupOptions options)
- {
- var connections = await _ccuConnectionsStore.GetConnectionsAsync().ConfigureAwait(false);
-
- var connection = connections.FirstOrDefault(
- x => string.Equals(x.Name, options.ConnectionName, StringComparison.OrdinalIgnoreCase));
-
- if (connection is null)
- {
- _console.MarkupLine($"[bold red]Connection '{options.ConnectionName}' not found[/]");
- return -1;
- }
-
- var credentials = _ccuConnectionsStore.GetCredentials(connection);
-
- var backupService = _ccuBackupServiceBuilder
- .ForHost(connection.Url.Host)
- .WithCredentials(credentials)
- .Build();
-
- var outputFilePath = options.OutputFilePath
- ?? $"backup_{connection.Name}_{DateTime.Now:yyyyMMdd_HHmmss}.tar.gz";
-
- await _console.Status()
- .StartAsync($"Creating backup for '{connection.Name}'...", async _ =>
- {
- await backupService.SaveBackupAsync(outputFilePath).ConfigureAwait(false);
- })
- .ConfigureAwait(false);
-
- _console.MarkupLine($"[bold green]Backup saved to '{outputFilePath}'[/]");
-
- return CommandResult.Success;
- }
-}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs
deleted file mode 100644
index 931f7cb..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/Backup/CreateBackupOptions.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using CreativeCoders.SysConsole.Cli.Parsing;
-using JetBrains.Annotations;
-
-namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu.Backup;
-
-///
-/// Options for the CCU backup command.
-///
-[UsedImplicitly]
-public class CreateBackupOptions
-{
- ///
- /// Gets or sets the name of the CCU connection to back up.
- ///
- [OptionValue(0, IsRequired = true)]
- public string ConnectionName { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the output file path for the backup archive. When not specified, a default file name is generated.
- ///
- [OptionParameter('o', "output", HelpText = "Output file path for the backup archive (.tar.gz)")]
- public string? OutputFilePath { get; set; }
-}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs
deleted file mode 100644
index 2e95906..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Ccu/CcuCommandGroup.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using CreativeCoders.Cli.Core;
-using CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu;
-
-[assembly: CliCommandGroup([CcuCommandGroup.Name], "Commands for CCU operations")]
-
-namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Ccu;
-
-///
-/// Defines the command group for CCU operations.
-///
-public static class CcuCommandGroup
-{
- ///
- /// The name of the CCU command group.
- ///
- public const string Name = "ccu";
-}
From 956a68559400a2842235ec25568539086f595a79 Mon Sep 17 00:00:00 2001
From: darthsharp <48331467+darthsharp@users.noreply.github.com>
Date: Sun, 26 Apr 2026 15:54:38 +0200
Subject: [PATCH 06/16] refactor(homeMatic): remove unused HTTP package
reference, `Dispose` method, and redundant device export logic
---
.../CreativeCoders.HomeMatic.csproj | 4 ----
.../FirmwareBackup/FirmwareBackupResult.cs | 8 +-------
.../Device/Export/ExportDevicesCommand.cs | 15 ---------------
3 files changed, 1 insertion(+), 26 deletions(-)
diff --git a/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj b/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj
index 3890119..ce48c09 100644
--- a/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj
+++ b/source/CreativeCoders.HomeMatic/CreativeCoders.HomeMatic.csproj
@@ -15,8 +15,4 @@
-
-
-
-
diff --git a/source/CreativeCoders.HomeMatic/FirmwareBackup/FirmwareBackupResult.cs b/source/CreativeCoders.HomeMatic/FirmwareBackup/FirmwareBackupResult.cs
index 67e016a..84c7716 100644
--- a/source/CreativeCoders.HomeMatic/FirmwareBackup/FirmwareBackupResult.cs
+++ b/source/CreativeCoders.HomeMatic/FirmwareBackup/FirmwareBackupResult.cs
@@ -11,7 +11,7 @@ namespace CreativeCoders.HomeMatic.FirmwareBackup;
/// Always dispose the result to release the connection.
///
[PublicAPI]
-public sealed class FirmwareBackupResult : IAsyncDisposable, IDisposable
+public sealed class FirmwareBackupResult : IAsyncDisposable
{
private readonly IAsyncDisposable[] _additionalResources;
@@ -62,10 +62,4 @@ public async ValueTask DisposeAsync()
await resource.DisposeAsync().ConfigureAwait(false);
}
}
-
- ///
- public void Dispose()
- {
- DisposeAsync().AsTask().GetAwaiter().GetResult();
- }
}
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 069ca65..3fda6e1 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
@@ -22,21 +22,6 @@ protected override object TransformData(ICompleteCcuDevice device)
{
WriteIndented = true
});
- return new
- {
- Device = device.DeviceData,
- Channels = device.Channels.Select(x =>
- {
- var channel = new
- {
- Info = x.ChannelData,
- ParamSets = x.ParamSetValues
- };
-
- return channel;
- }),
- ParamSets = device.ParamSetValues
- };
}
protected override Task LoadDataAsync(IMultiCcuClient ccuClient, ExportDevicesOptions options)
From fd8aa20174702f24e6b5736111750ffe1caf8edc Mon Sep 17 00:00:00 2001
From: darthsharp <48331467+darthsharp@users.noreply.github.com>
Date: Sun, 26 Apr 2026 19:48:42 +0200
Subject: [PATCH 07/16] refactor(cli): remove outdated CLI command and update
base command structure
- Deleted deprecated `CliBaseCommand`, `CliCommandBase`, and associated CLI command implementations.
- Introduced `DataOutputCommandBase` for a streamlined and reusable command structure.
- Added new tests in `CreativeCoders.HomeMatic.Tools.Cli.Base.Tests` to validate `DataOutputCommandBase` behavior and functionality.
---
Directory.Packages.props | 1 +
HomeMatic.sln | 115 +++++++++-
.../CliBaseServiceCollectionExtensions.cs | 9 +-
.../Commanding/CliBaseCommand.cs | 22 --
.../Commanding/CliCommandBase.cs | 12 --
.../Commanding/CliCommandExecutor.cs | 35 ----
.../Commanding/ICliCommandExecutor.cs | 13 --
.../Commanding/IHomeMaticCliCommand.cs | 6 -
.../IHomeMaticCliCommandWithOptions.cs | 7 -
.../Commands/DataOutputCommandBase.cs | 127 ++++++++++++
.../Commands/DataOutputFormat.cs | 22 ++
.../Commands/IDataOutputOptions.cs | 18 ++
.../Commands/Output/DataOutputWriter.cs | 75 +++++++
.../Commands/Output/IDataOutputWriter.cs | 24 +++
.../Serialization/DataSerializerFactory.cs | 40 ++++
.../Commands/Serialization/IDataSerializer.cs | 19 ++
.../Serialization/IDataSerializerFactory.cs | 14 ++
.../Serialization/JsonDataSerializer.cs | 30 +++
.../Serialization/YamlDataSerializer.cs | 27 +++
...tiveCoders.HomeMatic.Tools.Cli.Base.csproj | 1 +
.../Test/TestCommand.cs | 53 -----
.../DataOutputCommandBaseAdditionalTests.cs | 103 +++++++++
.../Commands/DataOutputCommandBaseTests.cs | 196 ++++++++++++++++++
.../Commands/Output/DataOutputWriterTests.cs | 194 +++++++++++++++++
.../DataSerializerFactoryTests.cs | 96 +++++++++
.../Serialization/JsonDataSerializerTests.cs | 105 ++++++++++
.../Serialization/YamlDataSerializerTests.cs | 104 ++++++++++
...ders.HomeMatic.Tools.Cli.Base.Tests.csproj | 36 ++++
28 files changed, 1350 insertions(+), 154 deletions(-)
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandBase.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandExecutor.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/ICliCommandExecutor.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommand.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommandWithOptions.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputCommandBase.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputFormat.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/IDataOutputOptions.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/DataOutputWriter.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/IDataOutputWriter.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/DataSerializerFactory.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializer.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializerFactory.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/JsonDataSerializer.cs
create mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/YamlDataSerializer.cs
delete mode 100644 source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Test/TestCommand.cs
create mode 100644 tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/DataOutputCommandBaseAdditionalTests.cs
create mode 100644 tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/DataOutputCommandBaseTests.cs
create mode 100644 tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/Output/DataOutputWriterTests.cs
create mode 100644 tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/Serialization/DataSerializerFactoryTests.cs
create mode 100644 tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/Serialization/JsonDataSerializerTests.cs
create mode 100644 tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/Serialization/YamlDataSerializerTests.cs
create mode 100644 tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests.csproj
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0d9d967..93b3b95 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -24,5 +24,6 @@
+
\ No newline at end of file
diff --git a/HomeMatic.sln b/HomeMatic.sln
index 9b7c40b..15c4af7 100644
--- a/HomeMatic.sln
+++ b/HomeMatic.sln
@@ -80,66 +80,172 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__docs", "__docs", "{246443
docs\HomeMatic-XmlRpc.md = docs\HomeMatic-XmlRpc.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.HomeMatic.Tools.Cli.Base.Tests", "tests\CreativeCoders.HomeMatic.Tools.Cli.Base.Tests\CreativeCoders.HomeMatic.Tools.Cli.Base.Tests.csproj", "{E3F3D28A-919C-4283-AEBD-A915A3EFE047}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Debug|x64.Build.0 = Debug|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Debug|x86.Build.0 = Debug|Any CPU
{33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Release|Any CPU.Build.0 = Release|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Release|x64.ActiveCfg = Release|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Release|x64.Build.0 = Release|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Release|x86.ActiveCfg = Release|Any CPU
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23}.Release|x86.Build.0 = Release|Any CPU
{C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Debug|x64.Build.0 = Debug|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Debug|x86.Build.0 = Debug|Any CPU
{C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Release|x64.ActiveCfg = Release|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Release|x64.Build.0 = Release|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF}.Release|x86.Build.0 = Release|Any CPU
{5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Debug|x64.Build.0 = Debug|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Debug|x86.Build.0 = Debug|Any CPU
{5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Release|x64.ActiveCfg = Release|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Release|x64.Build.0 = Release|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Release|x86.ActiveCfg = Release|Any CPU
+ {5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5}.Release|x86.Build.0 = Release|Any CPU
{1DB2232F-17CF-40AF-9190-09441C860BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DB2232F-17CF-40AF-9190-09441C860BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Debug|x64.Build.0 = Debug|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Debug|x86.Build.0 = Debug|Any CPU
{1DB2232F-17CF-40AF-9190-09441C860BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1DB2232F-17CF-40AF-9190-09441C860BEB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Release|x64.ActiveCfg = Release|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Release|x64.Build.0 = Release|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Release|x86.ActiveCfg = Release|Any CPU
+ {1DB2232F-17CF-40AF-9190-09441C860BEB}.Release|x86.Build.0 = Release|Any CPU
{15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Debug|x64.Build.0 = Debug|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Debug|x86.Build.0 = Debug|Any CPU
{15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Release|x64.ActiveCfg = Release|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Release|x64.Build.0 = Release|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Release|x86.ActiveCfg = Release|Any CPU
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF}.Release|x86.Build.0 = Release|Any CPU
{91834652-D1F2-4563-B684-AAF776E6B341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91834652-D1F2-4563-B684-AAF776E6B341}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Debug|x64.Build.0 = Debug|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Debug|x86.Build.0 = Debug|Any CPU
{91834652-D1F2-4563-B684-AAF776E6B341}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91834652-D1F2-4563-B684-AAF776E6B341}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Release|x64.ActiveCfg = Release|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Release|x64.Build.0 = Release|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Release|x86.ActiveCfg = Release|Any CPU
+ {91834652-D1F2-4563-B684-AAF776E6B341}.Release|x86.Build.0 = Release|Any CPU
{A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Debug|x64.Build.0 = Debug|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Debug|x86.Build.0 = Debug|Any CPU
{A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Release|x64.ActiveCfg = Release|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Release|x64.Build.0 = Release|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Release|x86.ActiveCfg = Release|Any CPU
+ {A45980D7-8E11-4DF2-9802-D6E5E7056CD1}.Release|x86.Build.0 = Release|Any CPU
{FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Debug|x64.Build.0 = Debug|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Debug|x86.Build.0 = Debug|Any CPU
{FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Release|x64.ActiveCfg = Release|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Release|x64.Build.0 = Release|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Release|x86.ActiveCfg = Release|Any CPU
+ {FF9E9629-FB10-4D56-B8B1-AA65748FECA7}.Release|x86.Build.0 = Release|Any CPU
{3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Debug|x64.Build.0 = Debug|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Debug|x86.Build.0 = Debug|Any CPU
{3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Release|x64.ActiveCfg = Release|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Release|x64.Build.0 = Release|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Release|x86.ActiveCfg = Release|Any CPU
+ {3181E206-1B81-4D8C-BFF2-9125A39D99F5}.Release|x86.Build.0 = Release|Any CPU
{A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Debug|x64.Build.0 = Debug|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Debug|x86.Build.0 = Debug|Any CPU
{A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Release|x64.ActiveCfg = Release|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Release|x64.Build.0 = Release|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Release|x86.ActiveCfg = Release|Any CPU
+ {A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE}.Release|x86.Build.0 = Release|Any CPU
{822ECD72-5DB0-4637-B794-CE27B02827AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{822ECD72-5DB0-4637-B794-CE27B02827AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Debug|x64.Build.0 = Debug|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Debug|x86.Build.0 = Debug|Any CPU
{822ECD72-5DB0-4637-B794-CE27B02827AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{822ECD72-5DB0-4637-B794-CE27B02827AC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Release|x64.ActiveCfg = Release|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Release|x64.Build.0 = Release|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Release|x86.ActiveCfg = Release|Any CPU
+ {822ECD72-5DB0-4637-B794-CE27B02827AC}.Release|x86.Build.0 = Release|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Debug|x64.Build.0 = Debug|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Debug|x86.Build.0 = Debug|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Release|x64.ActiveCfg = Release|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Release|x64.Build.0 = Release|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Release|x86.ActiveCfg = Release|Any CPU
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {33773A69-F53A-4BC8-B3B9-564C8C9B0E23} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
+ {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
+ {13FED5D5-41CE-4DB5-A0FD-5A1EB5FBEF51} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
{B79F3B3E-C9CE-4629-ADE3-B1659AF9C673} = {13FED5D5-41CE-4DB5-A0FD-5A1EB5FBEF51}
{5F02B2B8-FC8C-44D8-A0CD-E9E31115EAB5} = {B79F3B3E-C9CE-4629-ADE3-B1659AF9C673}
- {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF} = {0CBB6422-3CF8-4014-B881-E48DBC964D99}
- {13FED5D5-41CE-4DB5-A0FD-5A1EB5FBEF51} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
- {33773A69-F53A-4BC8-B3B9-564C8C9B0E23} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
{1DB2232F-17CF-40AF-9190-09441C860BEB} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
- {C8E5E8C0-FAA5-49D3-B121-F5E5BBF7C0EF} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
+ {15C3A03E-F2A5-4497-B0DE-A9EB73C9E6AF} = {0CBB6422-3CF8-4014-B881-E48DBC964D99}
{91834652-D1F2-4563-B684-AAF776E6B341} = {5BD797BA-4D66-4F55-A55E-5F1063678D8B}
{A45980D7-8E11-4DF2-9802-D6E5E7056CD1} = {5BD797BA-4D66-4F55-A55E-5F1063678D8B}
{FF9E9629-FB10-4D56-B8B1-AA65748FECA7} = {7DBE5A9D-0500-4C35-A673-481C411B0FA1}
@@ -148,6 +254,7 @@ Global
{386F9478-8C54-4B48-A6C3-9F3D009A799C} = {73022F12-1D56-44FF-AFF1-9FEA43637836}
{A6E76585-17FA-4F7F-A8EE-C4C517C0ABEE} = {B79F3B3E-C9CE-4629-ADE3-B1659AF9C673}
{822ECD72-5DB0-4637-B794-CE27B02827AC} = {B79F3B3E-C9CE-4629-ADE3-B1659AF9C673}
+ {E3F3D28A-919C-4283-AEBD-A915A3EFE047} = {5BD797BA-4D66-4F55-A55E-5F1063678D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E5E58EB-0096-4ED2-B1DE-D7FC5951CAB7}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CliBaseServiceCollectionExtensions.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CliBaseServiceCollectionExtensions.cs
index 861326c..eb8c797 100644
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CliBaseServiceCollectionExtensions.cs
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CliBaseServiceCollectionExtensions.cs
@@ -1,5 +1,7 @@
using CreativeCoders.HomeMatic.Core;
using CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Output;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
using CreativeCoders.HomeMatic.Tools.Cli.Base.Connections;
using CreativeCoders.HomeMatic.Tools.Cli.Base.SharedData;
using Microsoft.Extensions.DependencyInjection;
@@ -12,8 +14,6 @@ public static class CliBaseServiceCollectionExtensions
public static void AddHomeMaticCliBase(this IServiceCollection services)
{
services.TryAddSingleton();
- services.TryAddSingleton();
-
services.TryAddSingleton();
services.TryAddSingleton();
@@ -21,6 +21,11 @@ public static void AddHomeMaticCliBase(this IServiceCollection services)
services.TryAddSingleton(sp =>
sp.GetRequiredService().BuildMultiCcuClient());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+
services.AddHomeMatic();
}
}
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
deleted file mode 100644
index af703c9..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliBaseCommand.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using CreativeCoders.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(IHomeMaticXmlRpcApiBuilder apiBuilder, ISharedData sharedData)
-{
- private readonly IHomeMaticXmlRpcApiBuilder _apiBuilder = Ensure.NotNull(apiBuilder);
-
- protected IHomeMaticXmlRpcApi BuildApi()
- {
- var cliData = SharedData.LoadCliData();
-
- return _apiBuilder
- .ForUrl(new Uri($"http://{cliData.CcuHost}:{CcuRpcPorts.HomeMaticIp}"))
- .Build();
- }
-
- protected ISharedData SharedData { get; } = Ensure.NotNull(sharedData);
-}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandBase.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandBase.cs
deleted file mode 100644
index 9f1d35f..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandBase.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding;
-
-public abstract class CliDataCommandBase : IHomeMaticCliCommandWithOptions
- where TOptions : class
-{
- public Task ExecuteAsync(TOptions options)
- {
- throw new NotImplementedException();
- }
-
- protected abstract Task LoadDataAsync();
-}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandExecutor.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandExecutor.cs
deleted file mode 100644
index 6c64d0c..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/CliCommandExecutor.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using CreativeCoders.Core;
-using CreativeCoders.Core.Reflection;
-using CreativeCoders.SysConsole.Cli.Actions;
-using CreativeCoders.SysConsole.Cli.Actions.Exceptions;
-
-namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding;
-
-public class CliCommandExecutor : ICliCommandExecutor
-{
- private readonly IServiceProvider _serviceProvider;
-
- public CliCommandExecutor(IServiceProvider serviceProvider)
- {
- _serviceProvider = Ensure.NotNull(serviceProvider);
- }
-
- public async Task ExecuteAsync(TOptions options)
- where TCommand : IHomeMaticCliCommandWithOptions where TOptions : class
- {
- var command = typeof(TCommand).CreateInstance>(_serviceProvider);
-
- return command != null
- ? new CliActionResult(await command.ExecuteAsync(options))
- : throw new CliActionException("Command object cannot be created");
- }
-
- public async Task ExecuteAsync() where TCommand : IHomeMaticCliCommand
- {
- var command = typeof(TCommand).CreateInstance(_serviceProvider);
-
- return command != null
- ? new CliActionResult(await command.ExecuteAsync())
- : throw new CliActionException("Command object cannot be created");
- }
-}
\ No newline at end of file
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/ICliCommandExecutor.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/ICliCommandExecutor.cs
deleted file mode 100644
index 193c697..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/ICliCommandExecutor.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using CreativeCoders.SysConsole.Cli.Actions;
-
-namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding;
-
-public interface ICliCommandExecutor
-{
- Task ExecuteAsync(TOptions options)
- where TCommand : IHomeMaticCliCommandWithOptions
- where TOptions : class;
-
- Task ExecuteAsync()
- where TCommand : IHomeMaticCliCommand;
-}
\ No newline at end of file
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommand.cs
deleted file mode 100644
index 391c1d8..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommand.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding;
-
-public interface IHomeMaticCliCommand
-{
- Task ExecuteAsync();
-}
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
deleted file mode 100644
index 28338f8..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commanding/IHomeMaticCliCommandWithOptions.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding;
-
-public interface IHomeMaticCliCommandWithOptions
- where TOptions : class
-{
- Task ExecuteAsync(TOptions options);
-}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputCommandBase.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputCommandBase.cs
new file mode 100644
index 0000000..3f5454d
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputCommandBase.cs
@@ -0,0 +1,127 @@
+using CreativeCoders.Cli.Core;
+using CreativeCoders.Core;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Output;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
+using Spectre.Console;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands;
+
+///
+/// Generic base class for CLI commands that load arbitrary data, serialize it as JSON or YAML
+/// and write the result either to a file or to stdout.
+///
+/// The type of data produced by .
+/// The type of CLI options. Must implement .
+public abstract class DataOutputCommandBase : ICliCommand
+ where TOptions : class, IDataOutputOptions
+{
+ private readonly IAnsiConsole _console;
+
+ private readonly IDataSerializerFactory _serializerFactory;
+
+ private readonly IDataOutputWriter _outputWriter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The console used for status messages and stdout output.
+ /// The factory that resolves serializers for a format.
+ /// The writer that targets file or stdout.
+ protected DataOutputCommandBase(
+ IAnsiConsole console,
+ IDataSerializerFactory serializerFactory,
+ IDataOutputWriter outputWriter)
+ {
+ _console = Ensure.NotNull(console);
+ _serializerFactory = Ensure.NotNull(serializerFactory);
+ _outputWriter = Ensure.NotNull(outputWriter);
+ }
+
+ ///
+ /// Gets the console available to subclasses for additional output.
+ ///
+ protected IAnsiConsole Console => _console;
+
+ ///
+ public async Task ExecuteAsync(TOptions options)
+ {
+ Ensure.NotNull(options);
+
+ var format = _outputWriter.ResolveFormat(options.OutputFormat, options.OutputFile);
+
+ await OnBeforeLoadAsync(options).ConfigureAwait(false);
+
+ var data = await LoadDataAsync(options).ConfigureAwait(false);
+
+ var transformed = TransformData(data, options);
+
+ Ensure.NotNull(transformed);
+
+ var serializer = _serializerFactory.Create(format);
+ var content = serializer.Serialize(transformed);
+
+ await OnBeforeWriteAsync(options, format).ConfigureAwait(false);
+
+ await _outputWriter.WriteAsync(content, options.OutputFile).ConfigureAwait(false);
+
+ await OnAfterWriteAsync(options, format).ConfigureAwait(false);
+
+ return CommandResult.Success;
+ }
+
+ ///
+ /// Loads the data to be serialized.
+ ///
+ /// The CLI options.
+ /// The loaded data.
+ protected abstract Task LoadDataAsync(TOptions options);
+
+ ///
+ /// Transforms the loaded data into the object that is actually serialized. The default
+ /// implementation returns unchanged.
+ ///
+ /// The loaded data.
+ /// The CLI options.
+ /// The object passed to the serializer.
+ protected virtual object TransformData(TData data, TOptions options) => data!;
+
+ ///
+ /// Hook invoked before data is loaded. The default implementation writes a status message
+ /// to the console.
+ ///
+ /// The CLI options.
+ /// A task that completes when the hook is finished.
+ protected virtual Task OnBeforeLoadAsync(TOptions options)
+ {
+ _console.WriteLine("Loading data...");
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Hook invoked after serialization but before writing the result. The default implementation
+ /// writes a status message describing the resolved target.
+ ///
+ /// The CLI options.
+ /// The resolved output format.
+ /// A task that completes when the hook is finished.
+ protected virtual Task OnBeforeWriteAsync(TOptions options, DataOutputFormat resolvedFormat)
+ {
+ var target = string.IsNullOrWhiteSpace(options.OutputFile)
+ ? "stdout"
+ : $"file '{options.OutputFile}'";
+
+ _console.WriteLine($"Writing {resolvedFormat} output to {target}");
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Hook invoked after the result has been written. The default implementation is a no-op.
+ ///
+ /// The CLI options.
+ /// The resolved output format.
+ /// A task that completes when the hook is finished.
+ protected virtual Task OnAfterWriteAsync(TOptions options, DataOutputFormat resolvedFormat)
+ => Task.CompletedTask;
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputFormat.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputFormat.cs
new file mode 100644
index 0000000..6f9b685
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/DataOutputFormat.cs
@@ -0,0 +1,22 @@
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands;
+
+///
+/// Specifies the serialization format used to write CLI command output.
+///
+public enum DataOutputFormat
+{
+ ///
+ /// Format is derived from the output file extension; falls back to .
+ ///
+ Auto = 0,
+
+ ///
+ /// JSON output.
+ ///
+ Json = 1,
+
+ ///
+ /// YAML output.
+ ///
+ Yaml = 2
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/IDataOutputOptions.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/IDataOutputOptions.cs
new file mode 100644
index 0000000..0484094
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/IDataOutputOptions.cs
@@ -0,0 +1,18 @@
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands;
+
+///
+/// Contract for CLI command options that control serialized data output.
+///
+public interface IDataOutputOptions
+{
+ ///
+ /// Gets the desired output format. When set to
+ /// the format is derived from the output file extension.
+ ///
+ DataOutputFormat OutputFormat { get; }
+
+ ///
+ /// Gets the path of the output file. When null or empty the data is written to stdout.
+ ///
+ string? OutputFile { get; }
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/DataOutputWriter.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/DataOutputWriter.cs
new file mode 100644
index 0000000..70ede7f
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/DataOutputWriter.cs
@@ -0,0 +1,75 @@
+using CreativeCoders.Core;
+using CreativeCoders.Core.IO;
+using Spectre.Console;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Output;
+
+///
+/// Default implementation. Writes content either to a file via
+/// or to the provided .
+///
+public class DataOutputWriter : IDataOutputWriter
+{
+ private readonly IAnsiConsole _console;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The console used for stdout output.
+ public DataOutputWriter(IAnsiConsole console)
+ {
+ _console = Ensure.NotNull(console);
+ }
+
+ ///
+ public DataOutputFormat ResolveFormat(DataOutputFormat requestedFormat, string? outputFile)
+ {
+ var fromExtension = TryGetFormatFromExtension(outputFile);
+
+ if (fromExtension.HasValue)
+ {
+ return fromExtension.Value;
+ }
+
+ return requestedFormat == DataOutputFormat.Auto
+ ? DataOutputFormat.Json
+ : requestedFormat;
+ }
+
+ ///
+ public async Task WriteAsync(string content, string? outputFile)
+ {
+ Ensure.NotNull(content);
+
+ if (string.IsNullOrWhiteSpace(outputFile))
+ {
+ _console.WriteLine(content);
+ return;
+ }
+
+ await FileSys.File.WriteAllTextAsync(outputFile, content).ConfigureAwait(false);
+ }
+
+ private static DataOutputFormat? TryGetFormatFromExtension(string? outputFile)
+ {
+ if (string.IsNullOrWhiteSpace(outputFile))
+ {
+ return null;
+ }
+
+ var extension = Path.GetExtension(outputFile);
+
+ if (string.IsNullOrEmpty(extension))
+ {
+ return null;
+ }
+
+ return extension.ToLowerInvariant() switch
+ {
+ ".json" => DataOutputFormat.Json,
+ ".yaml" => DataOutputFormat.Yaml,
+ ".yml" => DataOutputFormat.Yaml,
+ _ => null
+ };
+ }
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/IDataOutputWriter.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/IDataOutputWriter.cs
new file mode 100644
index 0000000..4e97bb0
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Output/IDataOutputWriter.cs
@@ -0,0 +1,24 @@
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Output;
+
+///
+/// Resolves the effective output format and writes serialized data either to a file or to stdout.
+///
+public interface IDataOutputWriter
+{
+ ///
+ /// Determines the effective based on the requested format
+ /// and the output file extension.
+ ///
+ /// The format requested via options.
+ /// The path of the output file, or null/empty for stdout.
+ /// The resolved, concrete output format.
+ DataOutputFormat ResolveFormat(DataOutputFormat requestedFormat, string? outputFile);
+
+ ///
+ /// Writes the serialized to the configured target.
+ ///
+ /// The already serialized content.
+ /// The target file path. When null or empty, the content is written to stdout.
+ /// A task that completes when the content has been written.
+ Task WriteAsync(string content, string? outputFile);
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/DataSerializerFactory.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/DataSerializerFactory.cs
new file mode 100644
index 0000000..14a2c2f
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/DataSerializerFactory.cs
@@ -0,0 +1,40 @@
+using CreativeCoders.Core;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
+
+///
+/// Default implementation backed by all registered
+/// instances.
+///
+public class DataSerializerFactory : IDataSerializerFactory
+{
+ private readonly Dictionary _serializers;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// All registered serializers.
+ public DataSerializerFactory(IEnumerable serializers)
+ {
+ _serializers = Ensure.NotNull(serializers).ToDictionary(s => s.Format);
+ }
+
+ ///
+ public IDataSerializer Create(DataOutputFormat format)
+ {
+ if (format == DataOutputFormat.Auto)
+ {
+ throw new ArgumentException(
+ "Format must be resolved to a concrete value before requesting a serializer.",
+ nameof(format));
+ }
+
+ if (!_serializers.TryGetValue(format, out var serializer))
+ {
+ throw new InvalidOperationException(
+ $"No serializer registered for format '{format}'.");
+ }
+
+ return serializer;
+ }
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializer.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializer.cs
new file mode 100644
index 0000000..9819ff8
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializer.cs
@@ -0,0 +1,19 @@
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
+
+///
+/// Serializes arbitrary data into a string representation for a specific output format.
+///
+public interface IDataSerializer
+{
+ ///
+ /// Gets the format produced by this serializer.
+ ///
+ DataOutputFormat Format { get; }
+
+ ///
+ /// Serializes the given to its string representation.
+ ///
+ /// The data to serialize.
+ /// The serialized representation of the data.
+ string Serialize(object data);
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializerFactory.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializerFactory.cs
new file mode 100644
index 0000000..709afc6
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/IDataSerializerFactory.cs
@@ -0,0 +1,14 @@
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
+
+///
+/// Creates instances for a requested .
+///
+public interface IDataSerializerFactory
+{
+ ///
+ /// Returns the registered for the given .
+ ///
+ /// The desired output format. Must not be .
+ /// A serializer that produces output in .
+ IDataSerializer Create(DataOutputFormat format);
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/JsonDataSerializer.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/JsonDataSerializer.cs
new file mode 100644
index 0000000..066cb5a
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/JsonDataSerializer.cs
@@ -0,0 +1,30 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using CreativeCoders.Core;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
+
+///
+/// implementation that produces indented JSON using
+/// System.Text.Json.
+///
+public class JsonDataSerializer : IDataSerializer
+{
+ private static readonly JsonSerializerOptions Options = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ };
+
+ ///
+ public DataOutputFormat Format => DataOutputFormat.Json;
+
+ ///
+ public string Serialize(object data)
+ {
+ Ensure.NotNull(data);
+
+ return JsonSerializer.Serialize(data, Options);
+ }
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/YamlDataSerializer.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/YamlDataSerializer.cs
new file mode 100644
index 0000000..b54c94c
--- /dev/null
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/Commands/Serialization/YamlDataSerializer.cs
@@ -0,0 +1,27 @@
+using CreativeCoders.Core;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
+
+///
+/// implementation that produces YAML using YamlDotNet.
+///
+public class YamlDataSerializer : IDataSerializer
+{
+ private static readonly ISerializer Serializer = new SerializerBuilder()
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
+ .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
+ .Build();
+
+ ///
+ public DataOutputFormat Format => DataOutputFormat.Yaml;
+
+ ///
+ public string Serialize(object data)
+ {
+ Ensure.NotNull(data);
+
+ return Serializer.Serialize(data);
+ }
+}
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CreativeCoders.HomeMatic.Tools.Cli.Base.csproj b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CreativeCoders.HomeMatic.Tools.Cli.Base.csproj
index 9ca760f..50a7d4e 100644
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CreativeCoders.HomeMatic.Tools.Cli.Base.csproj
+++ b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Base/CreativeCoders.HomeMatic.Tools.Cli.Base.csproj
@@ -12,6 +12,7 @@
+
diff --git a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Test/TestCommand.cs b/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Test/TestCommand.cs
deleted file mode 100644
index 9085569..0000000
--- a/source/Tools/Cli/CreativeCoders.HomeMatic.Tools.Cli.Commands/Test/TestCommand.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System.Text.Json;
-using CreativeCoders.Cli.Core;
-using CreativeCoders.Core;
-using CreativeCoders.HomeMatic.JsonRpc.Api;
-using CreativeCoders.HomeMatic.Tools.Cli.Base.Commanding;
-using CreativeCoders.HomeMatic.Tools.Cli.Base.SharedData;
-using CreativeCoders.HomeMatic.XmlRpc.Client;
-using Spectre.Console;
-
-namespace CreativeCoders.HomeMatic.Tools.Cli.Commands.Test;
-
-public class TestCommand(
- IAnsiConsole console,
- IHomeMaticXmlRpcApiBuilder apiBuilder,
- ISharedData sharedData,
- IHomeMaticJsonRpcApiBuilder jsonRpcApiBuilder)
- : CliBaseCommand(apiBuilder, sharedData), ICliCommand
-{
- private readonly IAnsiConsole _console = Ensure.NotNull(console);
-
- public async Task ExecuteAsync()
- {
- var cliData = SharedData.LoadCliData();
- if (!cliData.Users.TryGetValue(cliData.CcuHost, out var userName))
- {
- userName = await _console.PromptAsync(new TextPrompt("User name: "));
- cliData.Users[cliData.CcuHost] = userName;
-
- SharedData.SaveCliData(cliData);
- }
-
- var api = jsonRpcApiBuilder.ForUrl(new Uri($"http://{cliData.CcuHost}/api/homematic.cgi")).Build();
-
- var loginResponse = await api.LoginAsync(userName, SharedData.GetPassword(cliData.CcuHost));
-
- _console.WriteLine($"Login Response: {JsonSerializer.Serialize(loginResponse)}");
-
- if (loginResponse.Result == null)
- {
- return 1;
- }
-
- var listAllDetailsResponse = await api.ListAllDetailsAsync(loginResponse.Result);
-
- _console.WriteLine($"All Details Response: {JsonSerializer.Serialize(listAllDetailsResponse)}");
-
- var logoutResponse = await api.LogoutAsync(loginResponse.Result);
-
- _console.WriteLine($"Logout Response: {JsonSerializer.Serialize(logoutResponse)}");
-
- return CommandResult.Success;
- }
-}
diff --git a/tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/DataOutputCommandBaseAdditionalTests.cs b/tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/DataOutputCommandBaseAdditionalTests.cs
new file mode 100644
index 0000000..3438218
--- /dev/null
+++ b/tests/CreativeCoders.HomeMatic.Tools.Cli.Base.Tests/Commands/DataOutputCommandBaseAdditionalTests.cs
@@ -0,0 +1,103 @@
+using AwesomeAssertions;
+using CreativeCoders.Cli.Core;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Commands;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Output;
+using CreativeCoders.HomeMatic.Tools.Cli.Base.Commands.Serialization;
+using FakeItEasy;
+using Spectre.Console;
+
+namespace CreativeCoders.HomeMatic.Tools.Cli.Base.Tests.Commands;
+
+public class DataOutputCommandBaseAdditionalTests
+{
+ private sealed class TestOptions : IDataOutputOptions
+ {
+ public DataOutputFormat OutputFormat { get; init; } = DataOutputFormat.Auto;
+
+ public string? OutputFile { get; init; }
+ }
+
+ private sealed class ConfigurableCommand : DataOutputCommandBase
+ {
+ public ConfigurableCommand(
+ IAnsiConsole console,
+ IDataSerializerFactory factory,
+ IDataOutputWriter writer)
+ : base(console, factory, writer)
+ {
+ }
+
+ public Func>? LoadFunc { get; init; }
+
+ public Func? TransformFunc { get; init; }
+
+ protected override Task LoadDataAsync(TestOptions options)
+ => LoadFunc is null ? Task.FromResult("data") : LoadFunc(options);
+
+ protected override object TransformData(string data, TestOptions options)
+ => TransformFunc is null ? data : TransformFunc(data, options)!;
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WhenLoadDataThrows_ExceptionPropagates()
+ {
+ // Arrange
+ var sut = new ConfigurableCommand(
+ A.Fake(),
+ A.Fake(),
+ A.Fake())
+ {
+ LoadFunc = _ => throw new InvalidOperationException("boom")
+ };
+
+ // Act
+ var act = async () => await sut.ExecuteAsync(new TestOptions());
+
+ // Assert
+ await act.Should().ThrowAsync().WithMessage("boom");
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WhenTransformReturnsNull_Throws()
+ {
+ // Arrange
+ var sut = new ConfigurableCommand(
+ A.Fake(),
+ A.Fake(),
+ A.Fake())
+ {
+ TransformFunc = (_, _) => null
+ };
+
+ // Act
+ var act = async () => await sut.ExecuteAsync(new TestOptions());
+
+ // Assert
+ await act.Should().ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WhenWriterThrows_ExceptionPropagates()
+ {
+ // Arrange
+ var serializer = A.Fake();
+ A.CallTo(() => serializer.Serialize(A