Skip to content

Leberkas-org/TurboHomeConnect

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TurboHomeConnect

Channel-based streaming client for the Home Connect API, built on Akka.Streams and TurboHTTP.

Follows the Turbo Client Pattern: commands flow into a channel, responses and events flow back out through another channel, and the materialized Akka.Streams flow is owned by an actor for clean lifecycle management.

Requirements

Install

dotnet add package TurboHomeConnect

Quick start with Docker

The sample app ships with a Dockerfile + docker-compose.yml + .env. Three steps:

  1. Register an app at https://developer.home-connect.com (one for the simulator is enough to start). Pick http://localhost:5099/oauth/callback as a redirect URI.
  2. Copy your client id/secret into .env:
    cp .env.example .env
    $EDITOR .env   # fill HOMECONNECT_CLIENT_ID and HOMECONNECT_CLIENT_SECRET
  3. Run it:
    docker compose up --build

The sample prints the authorize URL on first run — click it, authorize in your browser, the redirect lands back on the container's /oauth/callback, and the refresh token is persisted to ./.tokens/token.json on the host. Subsequent runs skip the interactive step.

.env knobs:

Variable Purpose
HOMECONNECT_CLIENT_ID / _SECRET From the developer portal.
HOMECONNECT_USE_SIMULATOR 1 for simulator, 0 for production.
HOMECONNECT_OPEN_BROWSER 0 for headless — URL is printed instead.
HOMECONNECT_TOKEN_FILE Where to persist tokens (inside the container, mounted as a volume).

Quick start — full surface in five lines

using Akka.Actor;
using TurboHomeConnect;
using TurboHomeConnect.Commands;

using var system = ActorSystem.Create("home-connect");
using var client = HomeConnectBuilder.Create()
    .UseProduction()                              // or .UseSimulator()
    .StaticAccessToken(myAccessToken)             // or .TokenProvider(...)
    .Build(system);

var appliances = await client.RequestAsync(new GetAppliancesCommand());
foreach (var a in appliances.Appliances) Console.WriteLine($"{a.HaId}{a.Type}");

Three usage patterns

1. RequestAsync — typed REST request/response

var status = await client.RequestAsync(new GetStatusCommand(haId));
foreach (var s in status.Status) Console.WriteLine($"{s.Key} = {s.Value}");

RequestAsync returns the concrete typed response (StatusResponse, AppliancesResponse, etc.) — no casting or pattern matching needed. On HTTP errors it throws HomeConnectApiException with StatusCode, ErrorKey, and CorrelationId.

2. SendAsync — fire-and-forget, response arrives on the channel

await client.SendAsync(new SetSettingCommand(haId, "BSH.Common.Setting.PowerState",
    JsonSerializer.SerializeToElement("BSH.Common.EnumType.PowerState.On")));

await foreach (var msg in client.Responses.ReadAllAsync())
{
    if (msg is SettingUpdatedResponse u) Console.WriteLine($"set {u.SettingKey}");
}

3. Subscribe — live SSE event stream

await client.SendAsync(new SubscribeEventsCommand());   // or scope to one haId

await foreach (var msg in client.Responses.ReadAllAsync())
{
    switch (msg)
    {
        case HomeConnectEventMessage e:
            foreach (var item in e.Items) Console.WriteLine($"{e.HaId}: {item.Key} = {item.Value}");
            break;
        case SubscriptionDisconnectedMessage drop:
            Console.WriteLine($"reconnecting after: {drop.Reason?.Message}");
            break;
    }
}

The SSE stream is wrapped in RestartSource.WithBackoff, so transient disconnects reconnect automatically with backoff. Tune via .SseRestartSettings(...) on the builder.

OAuth — two ways

A. Bring your own token (simplest)

Use any OAuth library you like (e.g. Duende.IdentityModel) and hand the builder a function that returns a current access token. The function is called for every REST request and every (re)subscribe, so it's the right place to refresh on expiry.

.TokenProvider(ct => myAuthClient.GetAccessTokenAsync(ct))

B. Built-in Authorization Code helper

using var oauth = new HomeConnectAuthorizationCodeFlow(new HomeConnectOAuthOptions
{
    ClientId      = "...",
    ClientSecret  = "...",
    RedirectUri   = new Uri("http://localhost:5099/oauth/callback"),
    TokenStore    = new InMemoryTokenStore(),     // implement IPersistedTokenStore for disk-backed
});

await oauth.AuthorizeInteractiveAsync();          // launches browser once
// then plug in:
.TokenProvider(oauth.GetAccessTokenAsync)

The helper hosts a local HttpListener for the OAuth callback and refreshes tokens automatically.

Configuration knobs

Builder method What it does Default
UseProduction() / UseSimulator() Sets base address. (required)
BaseAddress(uri) Custom base address (e.g. test proxy).
TokenProvider(func) Bearer token source. Called per request and per (re)connect. (required)
StaticAccessToken(s) Convenience for short-lived demos.
UseHttpClient(client) Bring your own ITurboHttpClient (DI-friendly). internal
ConfigureHttpClient(action) Tweak the internal TurboClientOptions. defaults
LoggerFactory(factory) Bridges Akka logs to MEL.
CommandCapacity(n) Bounded input channel capacity. 64
RestParallelism(n) In-flight REST requests. 4
MaxConcurrentSubscriptions(n) Parallel SSE streams. 4
SseRestartSettings(s) Backoff + max-restart policy for SSE. 1s/60s/0.2

4. Custom commands — wrap endpoints the library doesn't cover

public sealed record GetFridgeDoorStateCommand(string HaId) : RestCommand<FridgeDoorResponse>
{
    protected override HttpRequestMessage BuildRequest()
        => RestHelpers.Get($"api/homeappliances/{HaId}/status/Refrigeration.Common.Status.Door");

    protected override async Task<FridgeDoorResponse> MapResponseAsync(
        HttpResponseMessage response, CancellationToken ct)
    {
        var data = await RestHelpers.ReadDataAsync(response, MyJsonContext.Default.DataEnvelopeDoorState, ct);
        return new FridgeDoorResponse(CorrelationId, HaId, data.IsOpen);
    }
}

RestHelpers and DataEnvelope<T> are public — reuse the BSH media type plumbing rather than reimplementing it.

5. Bring your own stream — Akka.Streams integration

The client exposes both channel endpoints for direct stream composition:

// feed commands from a custom Akka.Streams graph
mySource.RunWith(ChannelSink.FromWriter(client.Commands, isOwner: false), materializer);

// pump all messages into an actor
ChannelSource.FromReader(client.Responses)
    .RunWith(Sink.ActorRefWithBackpressure<IHomeConnectMessage>(
        myActor, initMessage, ackMessage, completeMessage, ex => new StreamFailed(ex)),
        materializer);

Two caveats: responses claimed by a pending RequestAsync are intercepted and never appear on Responses; multiple Responses readers compete (each message goes to exactly one reader).

Architecture

 user code                channels                StreamOwnerActor
 ─────────                ────────                ─────────────────
 SendAsync(cmd) ─────► commandChannel ─────►   Partition ┬─► RestFlow ──► HttpClient ─┐
                                                         │                            │
                                                         └─► SseFlow ──► SseSource   ─┤
                                                                                      ▼
 client.Responses  ◄── userChannel ◄── Dispatch ◄── streamOutput ◄── Merge ◄── messages
                          │
                  ┌───────┴───────┐
                  │  correlated?  │
                  └───┬───────┬───┘
                  yes │       │ no
                      ▼       ▼
              pending TCS    user channel
                      │       │
                      ▼       ▼
             RequestAsync()  Responses.ReadAllAsync()

See turbo-client-pattern.md for the full pattern this library implements.

Coverage

REST endpoints covered:

  • Appliances: list, get
  • Status: list, single
  • Settings: list, single, set
  • Programs: active (get/start/stop), selected (get/select), available (list/get)
  • Program options: active/selected (list/get/set)
  • Commands: list, execute
  • Images: list, fetch bytes

Plus the SSE /events stream — both per-appliance and account-wide.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages