Pacem.Domotics.Legrand 0.10.19

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Pacem.Domotics.Legrand --version 0.10.19
                    
NuGet\Install-Package Pacem.Domotics.Legrand -Version 0.10.19
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pacem.Domotics.Legrand" Version="0.10.19" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pacem.Domotics.Legrand" Version="0.10.19" />
                    
Directory.Packages.props
<PackageReference Include="Pacem.Domotics.Legrand" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pacem.Domotics.Legrand --version 0.10.19
                    
#r "nuget: Pacem.Domotics.Legrand, 0.10.19"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pacem.Domotics.Legrand@0.10.19
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pacem.Domotics.Legrand&version=0.10.19
                    
Install as a Cake Addin
#tool nuget:?package=Pacem.Domotics.Legrand&version=0.10.19
                    
Install as a Cake Tool

Pacem.Domotics.Legrand

Drives the BTicino Smarther (Smarther 1, X8000 family, "Thermostat" app) through the Legrand Smarther v2.0 cloud API on developer.legrand.com.

Setup (once)

  1. On developer.legrand.com subscribe to the Smarther v2.0 product: keep the Primary Key (subscription key).
  2. Register an app (scopes comfort.read comfort.write) with an https Reply URL; wait for the approval; note client_id and client_secret.
  3. Authenticate, once: await thermostat.ChallengeAsync() (any thermostat: the chain is per Legrand account).

From then on no interaction is needed: the library refreshes the access token before it expires and saves the rotated refresh token (90 days, renewed at every refresh) before using it.

One token chain per consumer: two processes sharing a chain invalidate each other.

Authentication

Thermostat is an IAuthenticationDevice<LegrandTokens> (Pacem.Domotics.Abstractions):

  • ChallengeAsync() is silent while the stored chain works (DeviceAuthentication.Authenticated). Otherwise it builds the consent URL from the options (ClientId, RedirectUri, a state signed with ClientSecret, valid ConsentLifetime) and hands it at once to the configured ILegrandConsentPrompt (AddPacemLegrand(…).WithConsentPrompt<T>()): opening a browser, pushing it to the UIs, whatever the host does. It then waits until the tokens arrive, the consent expires, or the call is cancelled (DeviceAuthenticationFailed, Retryable). Concurrent challenges share one consent; cancelling abandons it.
  • Whoever receives Legrand's redirect (the Reply URL host) calls ILegrandConsent.RedeemAsync(query): the state is checked (any host configured with the same app can check it), the code exchanged, the LegrandTokens returned and not stored. A refused consent (error=access_denied), a forged or expired state throw LegrandConsentException.
  • thermostat.AuthenticateAsync(tokens) takes the chain over: it is refreshed at once, which retires the sender's copy, saved, and the pending challenge completes. Also how a chain moves from one host to another.
  • With no prompt configured, a challenge that needs a consent fails (Retryable: false).

Usage

services.AddPacemLegrand(o =>
{
    o.SubscriptionKey = "...";
    o.ClientId = "...";
    o.ClientSecret = "...";
    o.RedirectUri = "https://.../callback";
})
.WithTokenStore<MyDurableTokenStore>(); // default: InMemoryLegrandTokenStore, lost at every restart

// as the owner sees them in the Thermostat app (or their GUIDs)
var thermostat = sp.GetRequiredService<ILegrandDeviceFactory>()
    .CreateThermostat(new LegrandDeviceAddress("Torrazza 42", "Ground Thermostat"));

await thermostat.ChallengeAsync();      // silent, or the consent page
await thermostat.RequestStatusAsync();  // resolves the names, then reads
await thermostat.SetTemperatureAsync(21.5, DateTimeOffset.Now.AddHours(1)); // MANUAL until…
await thermostat.ResumeScheduleAsync();                                     // AUTOMATIC, stored program
await thermostat.SetModeAsync(LegrandModes.Protection);

thermostat.Plant.Events.OnStatusAsync = ctx => { /* ... */ return Task.CompletedTask; };
await thermostat.Plant.RefreshAsync(); // every registered module, one call each

The library wraps the API's HttpClient: no background work, no timers, no filesystem. When to read the status again is the caller's business, unless the webhook below pushes it. Status and commands are per module (there is no plant-wide status). activationTime is the device's local time without an offset: the library reads and writes it with the offset of the device clock (time).

Addressing

A Smarther can't be discovered before the account is authenticated, so its owner names it: LegrandDeviceAddress(PlantIdOrName, ModuleIdOrName). Each value is the name shown in the Thermostat app (trimmed, case-insensitive) or the id, told apart by its GUID shape. On the first API call the plant is looked up in plants and the module among the plant's chronothermostats (topology); LegrandPlant.PlantId and Thermostat.ModuleId then hold the ids. A value matching nothing, or more than one, throws LegrandAddressException, whose Available lists what the account does have (name (id)). Authentication needs no resolution, so a device can be challenged before its names have been checked.

Push: C2C notifications (webhook)

services.AddPacemLegrand(o => { /* ... */ })
    .WithWebhook(baseUrl: "https://relay.example.org"); // path default "/legrand/webhook"

app.UsePacemLegrandWebhook();
  • Each plant subscribes on its first refresh, to {baseUrl}{path}/{clientId}/{SHA-256 of the access token}. When the access token rolls over, the next refresh drops this consumer's subscriptions of the plant (those carrying /{clientId}/; others are left alone) and subscribes the new URL. Subscription failures are raised through Events.OnErrorAsync and never fail the refresh.
  • Without baseUrl the origin (scheme + host) is taken from the request being served when the plant subscribes; no request, or not https, throws (into OnErrorAsync). Behind a reverse proxy, run UseForwardedHeaders first; if the app lives under a path base, pass baseUrl including it.
  • Legrand does not sign its calls: the key segment is the secret. The middleware answers 404 to unknown keys (the current access token's, and the current and previous subscription keys are accepted), 400 to unreadable JSON, otherwise 200 at once (Legrand allows 750 ms and bans failing endpoints), then applies the notification to the registered thermostat and raises the plant's Events.OnStatusAsync, as a refresh would. Fields missing from a notification keep their last known value. Handler errors are logged, never answered.
  • The body may be an array of envelopes, a single envelope { id, eventType, subject, eventTime, data }, or the bare {"chronothermostats":[…]}.
  • Out of band: when the notifications reach this host some other way (e.g. a public relay receives them and forwards key and body to the host that owns the tokens), call ILegrandWebhook.ReceiveAsync(key, body). It is registered by WithWebhook, checks the key and reads the body as the middleware does, and completes after the dispatch: Accepted, UnknownKey or Unreadable. Pass a baseUrl that points at the receiver; it may carry a path (e.g. https://relay.example.org/houses/h1) to tell the receiver whose notification it is.
  • Hosts other than ASP.NET Core can parse the body themselves and call ILegrandNotificationDispatcher.

Token store: the default InMemoryLegrandTokenStore loses the chain at every restart. Anything that outlives one run needs a durable ILegrandTokenStore, added with AddPacemLegrand(…).WithTokenStore<T>() (or the factory overload). An empty store makes every call throw LegrandReauthorizationRequiredException.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-ricci 0 10/28/2026
1.0.0-peano 43 10/27/2026
1.0.0-maiorana 52 10/27/2026
0.10.19 50 10/27/2026
0.10.19-galilei 49 10/27/2026
0.10.19-fermi 49 10/27/2026
0.10.19-degiorgi 57 10/26/2026
0.10.19-cardano 64 10/26/2026
0.10.19-bombelli 71 10/20/2026