Weather
An integration exposes weather by implementing IWeatherProvider. It lists one
WeatherStationInstance per configured location and resolves each one to an IWeatherStation that
returns a WeatherSnapshot. Map your source’s own condition codes and fields onto that snapshot; the
source itself stays inside your integration.
Quick start
Section titled “Quick start”using MacroDeck.Sdk;using MacroDeck.Sdk.Actions;using MacroDeck.Sdk.Weather;
public sealed class RooftopWeatherIntegration : IPluginIntegration, IWeatherProvider{ private readonly RooftopStation _station = new(new RooftopClient("http://192.168.1.40"));
public IReadOnlyList<IActionDefinition> Actions { get; } = [];
public IReadOnlyList<WeatherStationInstance> GetInstances() => [new WeatherStationInstance("rooftop", "Rooftop")];
public IWeatherStation? GetStation(string instanceId) => instanceId == "rooftop" ? _station : null;
public Task InitializeAsync(IIntegrationContext context) { _station.Start(); return Task.CompletedTask; }
public Task ShutdownAsync() => _station.StopAsync();}The user can now pick “Rooftop” as the location of a Weather widget.
Things to know:
- The interface gives you the widget only. Variables such as
{{ vars.rooftop_temperature }}come from your ownIVariableProvider. The built-in Weather integration declares itsweather_*variables itself, and its Weather details action lists only its own locations. - Return a cached snapshot. The host reads every station about once a minute. Refresh on your own, slower schedule instead of fetching on every call.
- Instance ids are local (
"rooftop"). The host qualifies them asintegrationId::rooftopand saves them in widgets, so keep them stable.
Pressing the widget
Section titled “Pressing the widget”A Weather widget without a flow of its own opens details when pressed, if the station it shows comes
from a provider that names a details action. Declare one of your own actions as DetailsAction:
public WeatherDetailsAction? DetailsAction { get; } = new("show-station", StationParameter: "station");- The host resolves the station the widget shows: the station saved in the widget, or, when none is saved,
the same first station the widget draws. It then runs that station’s provider’s action on the pressing
client, with the station’s qualified id (
integrationId::rooftop) inStationParameter. - Leave
DetailsActionat its defaultnulland pressing the widget runs nothing until the user adds a flow. The built-in Weather integration declares its Weather details action, which opens a dialog for its own locations. - Only clients with a screen run it; a hardware deck does not. A flow the user added to the widget always wins.
- An action that shows a dialog opens it with
context.Ui.ShowModalAsyncand serves it from your ownIUiProvider, as the built-in Weather integration does. - Out of process the action travels in the
weathercapability’sdescriberesult. A host older than this member ignores it, and a plugin built against an older SDK declares no details action.
Implementing the station
Section titled “Implementing the station”using MacroDeck.Sdk.Weather;
internal sealed class RooftopStation(RooftopClient client) : IWeatherStation{ private readonly CancellationTokenSource _stop = new(); private volatile WeatherSnapshot _current = WeatherSnapshot.Unavailable("Rooftop"); private Task _loop = Task.CompletedTask;
public Task<WeatherSnapshot> GetSnapshotAsync(CancellationToken ct) => Task.FromResult(_current);
public void Start() => _loop = Task.Run(() => RefreshLoopAsync(_stop.Token));
public async Task StopAsync() { await _stop.CancelAsync(); await _loop; }
private async Task RefreshLoopAsync(CancellationToken ct) { using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10)); do { try { var r = await client.GetReadingAsync(ct); _current = new WeatherSnapshot { IsAvailable = true, LocationName = "Rooftop", Unit = TemperatureUnit.Celsius, Temperature = r.TemperatureC, Condition = r.IsRaining ? WeatherCondition.Rain : WeatherCondition.Clear, IsDay = r.IsDaylight, Humidity = r.HumidityPercent, WindSpeed = r.WindKmh, WindDirection = r.WindDegrees }; } catch (HttpRequestException) { // Keep the last good snapshot. It stays unavailable only if we never had one. } } while (await WaitAsync(timer, ct)); }
private static async Task<bool> WaitAsync(PeriodicTimer timer, CancellationToken ct) { try { return await timer.WaitForNextTickAsync(ct); } catch (OperationCanceledException) { return false; } }}WeatherSnapshot.Unavailable(locationName) is the “no data” answer: IsAvailable = false, with only the
location name set. Start with it and keep the last good snapshot when a refresh fails, as the built-in
Open-Meteo station does. A brief outage then doesn’t blank the widget.
Telling Macro Deck the weather changed
Section titled “Telling Macro Deck the weather changed”Macro Deck reads your stations about once a minute on its own. After a station fetched new data, tell it so, and Weather widgets show the new conditions at once instead of up to a minute later. If your integration also declares weather variables, ask for those to be read again at the same moment, so a widget whose states follow them never disagrees with the Weather widget.
// at the end of each refresh, with the IIntegrationContext from InitializeAsync_ = context.WeatherProvider.NotifySnapshotsChangedAsync();_ = context.Variables.RequestRefreshAsync();NotifySnapshotsChangedAsyncis best effort and never throws. Notices that arrive while one is pending are merged into one read, so calling it once per refresh is enough.- It covers changed snapshots only. A changed list of stations is still a catalog change (see Over the plugin protocol).
- A Macro Deck older than this member ignores the notice and keeps reading the stations once a minute.
The snapshot
Section titled “The snapshot”new WeatherSnapshot{ IsAvailable = true, LocationName = "Berlin", Unit = TemperatureUnit.Celsius, Temperature = 18.4, ApparentTemperature = 17.1, Condition = WeatherCondition.PartlyCloudy, IsDay = true, Days = [new WeatherForecastDay(new DateOnly(2026, 9, 13), WeatherCondition.Rain, Min: 11, Max: 17)], Hours = [new WeatherHour(DateTimeOffset.Now, WeatherCondition.RainShowers, 16.2, PrecipitationProbability: 70)]};| Property | Meaning |
|---|---|
IsAvailable |
false when there is no data (network error, not loaded yet). |
LocationName |
Shown on the widget. |
Unit |
Celsius or Fahrenheit. Every temperature in the snapshot is already in this unit. |
Temperature, ApparentTemperature |
Current and “feels like”. null when unknown. |
Condition |
A WeatherCondition, used to pick the icon. |
IsDay |
Picks the day or night icon variant. |
Days |
WeatherForecastDay(Date, Condition, Min, Max), one per day. |
Hours |
WeatherHour(Time, Condition, Temperature, PrecipitationProbability) from the current hour on. May be empty. |
WindSpeed |
km/h with Celsius, mph with Fahrenheit. |
WindDirection |
Degrees the wind blows from: 0 = north, 90 = east. |
Humidity |
Relative humidity, 0-100. |
Precipitation |
This hour: mm with Celsius, inches with Fahrenheit. |
Sunrise, Sunset |
Today, with the location’s own offset. |
Everything past Days is optional. Leave out what your source does not report, and consumers treat it
as missing. WeatherCondition values: Unknown, Clear, MainlyClear, PartlyCloudy, Overcast,
Fog, Drizzle, Rain, FreezingRain, Snow, SnowGrains, RainShowers, SnowShowers,
Thunderstorm. Map anything you can’t place to Unknown.
Edge cases
Section titled “Edge cases”- A station disappears. Drop it from
GetInstancesand returnnullfromGetStation. Widgets pointing at it show as unavailable. - The list is briefly empty while your integration re-initialises. The host keeps showing the last stations and checks again shortly before blanking anything.
- Invalid or duplicate ids from
GetInstancesare skipped and logged. - Cancellation. Honour the token in
GetSnapshotAsyncif you ever do real work there. Returning a cached value, as above, never blocks. ProviderNameis optional. Leave it out and the integration’s name (the manifest name for a plugin) is used.
Over the plugin protocol
Section titled “Over the plugin protocol”Weather is fully supported out of process (capability kind weather). An unreachable plugin’s stations
read as an unavailable snapshot. The station list is a snapshot, so after your configuration adds or
removes a location, call CatalogChanged(CapabilityKinds.Weather) on an injected
IPluginCatalogNotifier. The weather sample does exactly this.
See Capability parity.
context.WeatherProvider.NotifySnapshotsChangedAsync() sends the weather host api’s snapshots-changed
operation (see WebSocket protocol). Macro Deck accepts it only from a
plugin whose weather capability was negotiated and answers CAPABILITY_UNAVAILABLE otherwise; an older
Macro Deck answers CAPABILITY_UNSUPPORTED. The SDK swallows both, so the call stays safe everywhere.
See also
Section titled “See also”- Variables - temperature and condition variables for templates.
- Setup flows - asking the user for a location.
- Localization
- Testing