Monitoring Gardena on NixOS with prometheus-gardena-exporter
7 min readYour Gardena setup already knows useful things. Soil humidity. Soil temperature. Whether a valve is open. Whether a sensor battery is about to ruin your weekend. The problem is that all of it lives inside a phone app, which is a terrible place for operational data.
If you already run Prometheus and Grafana, that is where this belongs.
prometheus-gardena-exporter is a Rust exporter for the Gardena smart system API. It handles the OAuth client credentials flow, discovers your Gardena location, pulls an initial snapshot, keeps a live WebSocket connection open for realtime updates, periodically reconciles state, and exposes the whole thing as Prometheus metrics. It also ships with a NixOS module, optional local scrape wiring, and a Grafana dashboard, so you do not have to spend your evening building plumbing before you can graph a watering zone.
The useful twist is water usage. Gardena does not give you a real flow meter reading here. The exporter models liters from valve runtime with a default liters-per-minute value, and lets you override that globally or per valve. So no, it is not a meter. But yes, it can still become a pretty useful guesstimate once you tune it to your actual zones.
How the data gets there
The path from Gardena to Grafana looks like this:
Gardena API
-> OAuth token
-> location snapshot
-> WebSocket updates
prometheus-gardena-exporter
-> /metrics
Prometheus
-> queries
Grafana
At startup, the exporter authenticates with the Husqvarna auth API using your application key and secret, discovers available locations, picks the configured location or auto-selects the only available one, and fetches a full location snapshot. After that it subscribes to the WebSocket stream and keeps an in-memory view of device and service state current. A periodic snapshot refresh reconciles anything the live stream might have missed.
That design matters because the Gardena API is not something you want to poll on every Prometheus scrape. The exporter absorbs the API weirdness once and presents a normal /metrics endpoint to the rest of your monitoring stack.
Getting it into your flake
You have two straightforward ways to consume this on NixOS.
Via my ijohanne NUR packages flake
If you already pull in my package set, import the module from there:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
ijohanne-nur.url = "github:ijohanne/nur-packages";
};
outputs = { nixpkgs, ijohanne-nur, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
ijohanne-nur.nixosModules.prometheus-gardena-exporter
./configuration.nix
];
};
};
}
Directly from the exporter flake
If you want to pin just this exporter and nothing else, pull it straight from the repo:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
prometheus-gardena-exporter.url = "github:ijohanne/prometheus-gardena-exporter";
};
outputs = { nixpkgs, prometheus-gardena-exporter, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
modules = [
prometheus-gardena-exporter.nixosModules.default
./configuration.nix
];
};
};
}
Same module either way. Pick whichever matches how you already manage third-party flake inputs.
Gardena application setup
Before NixOS gets involved, you need a Gardena application in the Husqvarna developer portal:
- Create or open an app at developer.husqvarnagroup.cloud/apps
- Add a redirect URL such as
http://localhost - Copy the application key and application secret
One detail worth calling out because it is mildly annoying the first time you hit it: the application key is used as both the X-Api-Key header and the OAuth client_id. The application secret is the OAuth client_secret.
The exporter includes a helper command that prints the raw token request:
nix run . -- print-token-curl
That expands to:
curl -fsSL -X POST -d "grant_type=client_credentials&client_id=$GARDENA_APPLICATION_KEY&client_secret=$GARDENA_APPLICATION_SECRET" \
https://api.authentication.husqvarnagroup.dev/v1/oauth2/token
If you want to test the auth flow directly:
export GARDENA_APPLICATION_KEY="..."
export GARDENA_APPLICATION_SECRET="..."
nix run . -- fetch-token --raw
To discover available locations:
nix run . -- list-locations \
--application-key "$GARDENA_APPLICATION_KEY" \
--application-secret "$GARDENA_APPLICATION_SECRET"
If the application only has access to one location, the exporter can auto-select it. If it has access to more than one, set locationId.
NixOS configuration
Here is the practical NixOS setup:
{
services.prometheus-gardena-exporter = {
enable = true;
enableLocalScraping = true;
enableGrafanaDashboard = true;
applicationKeyFile = /run/secrets/gardena-application-key;
applicationSecretFile = /run/secrets/gardena-application-secret;
locationId = "db789fe8-2af2-4eaf-a0ef-8ad795617971";
estimatedFlowLitersPerMinute = 3.5;
estimatedFlowLitersPerMinuteByValve = {
"5f7a3e6e-1111-2222-3333-444444444444" = 1.2;
"8c9d0a1b-5555-6666-7777-888888888888" = 6.0;
};
validateAuthOnStartup = true;
};
}
Three options do most of the quality-of-life work here:
enableLocalScraping = true;adds a Prometheus scrape target for youenableGrafanaDashboard = true;provisions the included Gardena dashboard automaticallyvalidateAuthOnStartup = true;fails fast if the app key or secret is wrong instead of looking “healthy” while doing nothing useful
There is also a nice Nix-specific detail in the module implementation: the service uses LoadCredential, so the application key and secret are read from files at runtime and never written into the Nix store.
By default the exporter listens on 127.0.0.1:9134, which is exactly what I want for a local Prometheus scrape.
Finding the valve IDs you actually need
Per-valve flow overrides are keyed by Gardena service_id, not by the friendly zone names from the app. That sounds annoying until you realize the exporter already has a helper for it:
nix run . -- list-valves \
--application-key "$GARDENA_APPLICATION_KEY" \
--application-secret "$GARDENA_APPLICATION_SECRET"
Without Nix:
cargo run -- list-valves \
--application-key "$GARDENA_APPLICATION_KEY" \
--application-secret "$GARDENA_APPLICATION_SECRET"
The output is tab-separated and includes:
location_id location device_id controller_name service_id valve_name
That gives you the stable service_id values you need for estimatedFlowLitersPerMinuteByValve.
Water usage is a model, not a meter
This is the part worth being explicit about.
Gardena cannot tell this exporter how many liters actually flowed through a zone. There is no physical flow sensor data here. What you get instead is a modeled estimate based on valve-open time.
The built-in default is 3.5 L/min. That number is derived from a rough monthly estimate:
5 m3/month = 5000 L/month
45 watering minutes/day x 30 days = 1350 watering minutes/month
5000 / 1350 = 3.7 L/min
The exporter rounds that down slightly and uses 3.5 L/min as a conservative default.
That default is fine as a starting point. It is also where people stop too early.
If one zone is a drip line and another is a spray-heavy bed, using one shared liters-per-minute value for both is going to produce nonsense-shaped precision. The fix is per-valve overrides:
services.prometheus-gardena-exporter = {
estimatedFlowLitersPerMinute = 3.5;
estimatedFlowLitersPerMinuteByValve = {
"5f7a3e6e-1111-2222-3333-444444444444" = 1.2;
"8c9d0a1b-5555-6666-7777-888888888888" = 6.0;
};
};
That is where the model gets much more believable. You are still estimating, but now you are estimating with zone-specific assumptions instead of pretending every part of the garden consumes water the same way.
This is most useful when:
- only one valve is active at a time
- each zone has a reasonably consistent emitter profile
- your water pressure is fairly stable
Treat the result as a guesstimate with sharp edges, not as a utility-grade reading. It is still good enough to answer real questions like “which zone is responsible for most of this month’s watering” or “did that new schedule double runtime for the thirsty bed again?”
What you get in Prometheus and Grafana
The exporter covers the useful things first.
On the exporter-health side, you get metrics like:
gardena_exporter_connectedgardena_exporter_last_successful_sync_timestamp_secondsgardena_exporter_websocket_reconnects_totalgardena_exporter_snapshot_refreshes_total
On the device side, you get:
gardena_sensor_soil_humidity_percentgardena_sensor_soil_temperature_celsiusgardena_sensor_ambient_temperature_celsiusgardena_sensor_light_intensity_luxgardena_device_battery_level_percentgardena_device_rf_link_level_percent
And on the watering side, the interesting metrics are:
gardena_valve_opengardena_valve_duration_secondsgardena_valve_estimated_water_liters_totalgardena_valve_estimated_current_water_flow_liters_per_minutegardena_estimated_water_liters_totalgardena_estimated_current_water_flow_liters_per_minute
The included Grafana dashboard leans into the practical views:
- exporter connection and active valve count
- average soil humidity and low-battery quick checks
- soil humidity and soil temperature by zone
- ambient temperature and light where the hardware exposes them
- valve status tables
- selected-range estimated water usage
- cumulative estimated water by zone
In other words, it starts where you would probably end up after an afternoon of dashboard-building anyway.
Sharp edges and limitations
The exporter is honest about the shape of the underlying API:
- snapshot endpoints are rate-limited, so the exporter serves cached state and does not poll on every scrape
- the WebSocket URL is short-lived, so the exporter requests it and connects immediately
- some Gardena sensors only report
soilHumidityandsoilTemperature; ambient temperature and light are optional - estimated water totals live in memory and reset when the exporter restarts
None of those are deal-breakers. They are just things you want to know before you build a monthly watering report and then restart the service halfway through the month.
Garden telemetry that behaves like infrastructure
That is really the point of this exporter.
Your irrigation system should not be a glossy black box that only exists in a mobile app. It should be queryable, graphable, alertable, and boring in the same way the rest of your stack is boring. Gardena provides the devices. Prometheus and Grafana provide the observability story. This exporter is the bridge between the two.
And the water estimation model is a good example of the overall approach: no fake certainty, no pretending the API provides data it does not. Just a useful, tunable approximation that gets better when you tell it what your valves actually do.
That is the kind of tradeoff I will take every time.