An OpenDeck profile generator plus a small custom plugin that puts your Nespresso Stats dashboard on a Stream Deck. Each capsule key logs a brew on press and shows that capsule's live remaining stock; two upkeep keys log cleaning / descaling; and a bottom-right button opens a second page of live coffee stats.
The deck in the picture: the top two rows are capsule keys (artwork + remaining stock), the bottom row is Descaled, Cleaned and the STATS button. Any key can point at a different capsule, so the layout is just a starting point.
Stream Deck key press ──HTTP──▶ nespresso-stats
▲ │
└──────── GET /api/state ──────┘ (key titles refresh every 10 s)
- OpenDeck running where the Stream Deck is.
- Node.js (OpenDeck ships its own;
wsbelow is only needed on Node 20). - A reachable nespresso-stats instance.
- Python 3 to run
install.py.
-
Clone and edit the deck.
CAPSULESandMAINTENANCEat the top ofinstall.pydefine the keys. Capsule names must match the names in your nespresso-stats dashboard exactly. The second tuple field is the artwork file name fromartwork/; set it toNoneto use the default icon. -
Install the plugin. Copy the bundled plugin folder into OpenDeck's
plugins/directory:OS OpenDeck config dir Linux ~/.config/opendeckmacOS ~/Library/Application Support/opendeckWindows %APPDATA%\opendeckCONFIG=~/.config/opendeck cp -r com.khmls.nespresso.sdPlugin "$CONFIG/plugins/" ( cd "$CONFIG/plugins/com.khmls.nespresso.sdPlugin" && npm install ) # Node 20 only
-
Generate and install the profile. Point
--urlat your dashboard:python3 install.py --url http://127.0.0.1:8787 --fill-titles
This writes
profiles/<device>/nespresso.jsonandnespresso-stats.jsonunder the OpenDeck config dir. Select the nespresso profile in OpenDeck.
If OpenDeck runs on another machine, pass --config-dir and --device (the
folder under profiles/) instead of relying on auto-detection.
The repo ships artwork/: one 176×176 PNG for every capsule in the
nespresso-stats catalogue (plus the cleaned / descaled upkeep keys), named
after the capsule — lower-case, spaces replaced by hyphens (Double Espresso Scuro → double-espresso-scuro.png). This is the default --images-dir, and
install.py copies every PNG in it into <config>/images/nespresso/, so the
whole pod-type library is available to add to keys later.
To use your own pictures instead, point --images-dir at a folder of PNGs:
python3 install.py --url http://127.0.0.1:8787 --images-dir ./my-art --fill-titlesKeys whose file is missing fall back to the plugin icon. The bundled artwork is AI-generated placeholder art, not official Nespresso assets.
The bottom-right key switches to a second profile (nespresso-stats) of 14 live
tiles; its top-left key switches back. The plugin renders each tile as an SVG
image built from /api/state + /api/stats, aggregating the metrics exactly
like the dashboard does.
Navigation uses OpenDeck's bundled starter-pack switchprofile action (only
that plugin may switch profiles), so the two navigation keys are plain OpenDeck
actions — the profile names are nespresso and nespresso-stats.
- On
willAppear/ settings change the plugin pollsGET /api/stateevery 10 s and writes each key's number withsetTitle— stock count, or days until upkeep is due (ceil(next - now),0when overdue or never logged). - A capsule press resolves the capsule id from the last poll and posts
POST /api/brew{"capsule_id": <id>, "delta": -1, "log": true, "source": "streamdeck"}, then refreshes immediately. If the name isn't in the dashboard it skips the press rather than logging an unattributed brew. - An upkeep press posts
POST /api/maintenance{"task": "clean"|"descale"}. - On the stats page the plugin fetches
/api/stats, aggregates today / last-7-days / average / month spend / stock-days-left / running-low / upkeep-days / a 12-week heatmap / family and capsule rankings, and sends each tile as an SVG image withsetImage(the SVG is re-sent when it changes, and at least every 30 s). - Per-key settings: capsule key
{"capsule": "<name>", "url": "<base>"}, upkeep key{"task": "clean"|"descale", "url": "<base>"}, stats key{"metric": "<id>", "url": "<base>"}(ids inSTATS_LAYOUT).NESPRESSO_POLL_MSoverrides the poll interval.
Titles are re-sent every poll, not just on change: OpenDeck ignores an unchanged
title, and re-sending self-heals an update that arrived before the frontend's key
was listening. Only changes are logged. npm install is only needed for the ws
WebSocket client that Node 20 lacks (Node 22+ uses the built-in one).
--url URL nespresso-stats base URL (default http://127.0.0.1:8787)
--fill-titles stamp current stock / days-until-due into the profile
(reads GET /api/state) -- recommended
--images-dir DIR folder of key artwork to copy (default: bundled artwork/)
--out FILE write the profile JSON here instead of installing
--stats with --print/--out, use the stats page instead of the main deck
--print print the profile instead of installing
--config-dir DIR OpenDeck config directory (skips auto-detection)
--device ID device folder under profiles/ (skips auto-detection)
--selftest check the profile builder
Always deploy with --fill-titles. The key renders whatever the profile
holds when it loads; the plugin's first setTitle can land before the frontend's
key is listening and OpenDeck ignores an unchanged title, so a profile with empty
titles can stay blank until something else changes. Baking the numbers in removes
that dependency; live updates keep working afterwards.
OpenDeck can run headless under Xvfb (e.g. on a Raspberry Pi), with the Stream
Deck attached to that machine. The service needs HID access as the user it runs
as. The vendor 40-streamdeck.rules relies on TAG+="uaccess" (which needs a
logind session); on a headless box add a rule granting the plugdev group, for
example /etc/udev/rules.d/99-streamdeck-plugdev.rules.
Deploy steps are the same three above: copy the plugin (npm install on Node 20),
copy the artwork, install the profile, then restart OpenDeck. OpenDeck rewrites
the profile in its canonical form on load and persists the live counts back into
states[].text, so the on-disk profile can lag the deck by a save cycle — the key
itself updates within one poll.
- Key shows
?— the capsule name ininstall.pydoesn't match any capsule in nespresso-stats, or the dashboard isn't reachable from the deck machine. - Press does nothing and the log says "no capsule id" — same name mismatch.
- Blank titles right after loading — deploy with
--fill-titles. - Blank stats tiles — they render only while the stats page is shown; check
the plugin log (
<data-dir>/opendeck/logs/plugins/com.khmls.nespresso.sdPlugin.log). - Plugin doesn't start — check OpenDeck's plugin log; on Node 20 run
npm installin the plugin folder.

