Pacem.Domotics.Legrand
0.10.19
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
<PackageReference Include="Pacem.Domotics.Legrand" Version="0.10.19" />
<PackageVersion Include="Pacem.Domotics.Legrand" Version="0.10.19" />
<PackageReference Include="Pacem.Domotics.Legrand" />
paket add Pacem.Domotics.Legrand --version 0.10.19
#r "nuget: Pacem.Domotics.Legrand, 0.10.19"
#:package Pacem.Domotics.Legrand@0.10.19
#addin nuget:?package=Pacem.Domotics.Legrand&version=0.10.19
#tool nuget:?package=Pacem.Domotics.Legrand&version=0.10.19
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)
- On developer.legrand.com subscribe to the Smarther v2.0 product: keep the Primary Key (subscription key).
- Register an app (scopes
comfort.read comfort.write) with an https Reply URL; wait for the approval; noteclient_idandclient_secret. - 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 withClientSecret, validConsentLifetime) and hands it at once to the configuredILegrandConsentPrompt(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, theLegrandTokensreturned and not stored. A refused consent (error=access_denied), a forged or expired state throwLegrandConsentException. 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 throughEvents.OnErrorAsyncand never fail the refresh. - Without
baseUrlthe origin (scheme + host) is taken from the request being served when the plant subscribes; no request, or not https, throws (intoOnErrorAsync). Behind a reverse proxy, runUseForwardedHeadersfirst; if the app lives under a path base, passbaseUrlincluding 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 byWithWebhook, checks the key and reads the body as the middleware does, and completes after the dispatch:Accepted,UnknownKeyorUnreadable. Pass abaseUrlthat 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 | Versions 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. |
-
net10.0
- Pacem.Domotics.Abstractions (>= 0.10.19)
- Pacem.Net.Http (>= 0.10.19)
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 |