An always-on information dashboard for the GeeekPi 11.26" 1920x440 HDMI LCD (capacitive touch), driven by a Raspberry Pi Zero 2W. Weather, markets, now-playing, AI usage, mail and a clock — in four columns across an ultrawide bar panel.
This project is a fork of czuryk/Waveshare-ePaper-10.85-dashboard by czuryk (Mike Sevbo) — an e-ink dashboard for the Waveshare 10.85" e-Paper HAT+. The widget set, the integrations (Strava, Bambu Lab, Roborock, Spotify, Gmail, weather) and the overall design are his work. All credit for the original concept and implementation goes to him.
This fork ports it from 1-bit e-paper to a full-colour HDMI LCD, which changed the renderer, the geometry, and the refresh model. See Changes from the e-paper original.
The upstream repository does not publish a LICENSE file. Please check with the original author before redistributing.
Four columns, left to right, exactly as pictured above.
- Bambu Lab 3D printer (top) — live print status, completion percentage, time remaining and current layer. Reads local MQTT over your LAN; shows
PRINTER: OFFLINEwhen the printer is powered down or unreachable. - Markets (bottom) — BTC, S&P 500 and gold (US $/oz), each with its daily % change, coloured green/red. Data from Yahoo Finance; no API key needed.
- Bluetooth now-playing (top) — pair your phone (the Pi advertises as
Pi_Dashboard) and it shows the current track, artist, elapsed/total time and a progress bar, read over BlueZ AVRCP while the phone plays from any app. Tap the on-screen ⏮ ⏯ ⏭ buttons to control playback. No audio backend — only the metadata rides the AVRCP control channel. Song and artist render with Noto Sans CJK, so non-Latin names display correctly. - Claude AI usage (bottom) — your 5-hour and 7-day limits as percentages with reset countdowns and progress bars.
- Current conditions — large temperature, a condition icon, the UV index (highlighted amber at 6+) and the resolved location name.
- Wind — a compass rose with a direction arrow and speed in km/h.
- Air quality — European AQI, coloured green / white / red by severity, inverted into a filled badge when it's high.
- 4-day forecast — weekday, condition icon and high/low for each of the next four days, with today's card highlighted.
Data comes from Open-Meteo (no API key). Location resolves in priority order — ZIP code, then city name, then public-IP geolocation, then a hardcoded lat/lon fallback. See Set your location.
- Clock — a large seven-segment time readout, plus date and weekday.
- Gmail — unread count for your primary inbox, with the envelope icon highlighted when mail is waiting.
- Status footer — last data refresh, the Pi's IP address, and a reminder of the touch gestures.
- Tap anywhere — force an immediate refetch of all data (rate-limited to once per 15 s).
- Press & hold ~1.2 s — toggle the light / dark theme.
- Press & hold 5 s — open the on-screen Settings menu.
- Screensaver — after 10 minutes idle the dashboard is replaced by a drifting clock on black, and 10 minutes after that the panel is powered off entirely; the next touch brings the dashboard straight back from either stage. With an HC-SR501 motion sensor fitted, walking up to the panel wakes it too. The idle timeout and the screen-off stage are both adjustable on-screen under Settings → Screensaver (code defaults live in
SCREENSAVER_SECONDS,SCREEN_OFF_SECONDSandSCREEN_OFF_ENABLEDinmain.py; aSCREENSAVER_SECONDSof0disables both).
These are inherited from the original project and are switched off in main.py. Each takes the slot of the widget listed, so enabling one replaces what's currently shown there.
| Widget | Flag | Takes the slot of | What it shows |
|---|---|---|---|
| Strava | ENABLE_STRAVA |
Bambu printer (col 1 top) | Total and yearly distance and ride counts, split by bike / hike |
| Roborock | ENABLE_ROBOROCK |
Now-playing (col 2 top) | Vacuum battery, status, and area cleaned during a run |
| Antigravity | ENABLE_ANTIGRAVITY |
Now-playing (col 2 top) | Antigravity usage limit and reset time |
| Spotify (via Last.fm) | ENABLE_SPOTIFY |
Claude usage (col 2 bottom) | Currently playing track and artist |
Where two widgets compete for a slot the first one wins: ENABLE_ROBOROCK beats ENABLE_ANTIGRAVITY beats the now-playing widget, and ENABLE_CLAUDE beats ENABLE_SPOTIFY. So to see Spotify you must also set ENABLE_CLAUDE = False.
If both ENABLE_CLAUDE and ENABLE_SPOTIFY are off, column 2's lower slot falls back to a time-progress widget (day / month / year elapsed). With everything off the dashboard boots into that fallback set and needs no credentials at all.
Setup for these lives in Optional integrations.
- Raspberry Pi Zero 2W (tested) with a mini-HDMI → HDMI adapter
- GeeekPi 11.26" 1920x440 HDMI LCD, capacitive touch
- HDMI cable + USB cable — the USB link carries the touch panel, which enumerates as a standard USB HID touchscreen, no driver needed
- microSD card, 8 GB or larger
- (optional) an HC-SR501 PIR motion sensor to wake the screensaver on approach — see Motion wake sensor
An HC-SR501 PIR sensor pointed out from the panel wakes the screensaver when it sees you move nearby, so the dashboard is already showing before you reach the screen. Without it (or if gpiozero isn't installed) nothing changes — the screensaver still wakes on touch.
Wire it to the Pi's 40-pin header using BCM pin numbers:
| HC-SR501 | Pi header | Note |
|---|---|---|
VCC |
5V (pin 2) | |
GND |
GND (pin 6) | |
OUT |
GPIO23 (pin 16) | 3.3V logic — connects directly, no divider |
The HC-SR501's OUT idles at 0V and pulses to 3.3V while it sees motion, so it wires straight to a GPIO. Its two on-board pots set sensitivity and how long OUT stays high after a trigger; the jumper picks single/repeat retrigger. Give it ~60s to settle after power-up.
Then install the GPIO libraries and test the wiring before starting the dashboard:
sudo apt install -y python3-gpiozero python3-lgpio
sudo python3 proximity.py # live OUT readout; prints *** WAKE *** on motionOnce it reports motion when you wave, restart the service. Motion wake can be turned on/off on-screen, along with the screensaver timeout — see the Settings menu. The pin is set near the top of main.py (PROXIMITY_OUT_PIN); set PROXIMITY_ENABLED = False to disable the sensor entirely.
Use Raspberry Pi OS Lite (64-bit) — the console-only image.
⚠️ Do not use a "with desktop" image. It runs its own Wayland compositor (labwc) which holds the screen and fightscagefor it, so the dashboard never appears. If you must, switch the Pi to boot to console:sudo systemctl set-default multi-user.target.
In Raspberry Pi Imager, click the gear icon before writing and set:
- hostname — e.g.
raspberrypi - Enable SSH — with a password or your public key
- Configure wireless LAN — your WiFi SSID, password and country code
- username / password — remember these; the systemd unit assumes
pi
This is the easiest way to get WiFi working, because the dashboard's own on-screen WiFi setup needs the dashboard to already be running. Once it is running you can switch networks from the touchscreen without a keyboard — see Changing WiFi later.
Boot the Pi, then SSH in:
ssh pi@raspberrypi.localfbset -s # should report 1920x440
⚠️ Do not force an HDMI mode. The panel advertises 1920x440 over EDID. Addinghdmi_timings/hdmi_modetoconfig.txt, orvideo=HDMI-A-1:…tocmdline.txt, gives a black screen. Let KMS pick the EDID mode and remove any such override.
If the panel comes up at another resolution the app scales frames to fit and logs a warning.
The dashboard renders through cage, a single-app Wayland kiosk compositor, with seatd providing the seat.
sudo apt update
sudo apt install -y git \
python3-pygame python3-pil python3-requests \
cage seatd wlr-randr \
python3-dbus-next bluez-tools fonts-noto-cjk
sudo systemctl enable --now seatdseatd must be enabled explicitly on a Lite image, or cage fails with a libseat error.
| Package | Why |
|---|---|
python3-pygame, python3-pil |
Rendering — Pillow composes the frame, pygame/SDL blits it |
cage, seatd, wlr-randr |
Kiosk compositor, its seat, and display control |
python3-dbus-next, bluez-tools |
Bluetooth now-playing widget and headless pairing |
fonts-noto-cjk |
Chinese / Japanese / Korean song and artist names |
git clone https://github.com/xiabo-lab/pi_dashboard.git ~/Pi_dashboard
cd ~/Pi_dashboardRaspberry Pi OS Bookworm enforces PEP 668, so anything not available from apt needs --break-system-packages:
pip3 install --break-system-packages paho-mqtt \
google-api-python-client google-auth-httplib2 google-auth-oauthlibpaho-mqtt— required by the Bambu Lab printer widget.- the three
google-*packages — required by the Gmail widget.
The bambulabs_api library is already bundled in lib/, so there is nothing to install for it.
Only if you plan to enable the Roborock widget:
pip3 install --break-system-packages roborock aiomqttLets the Pi accept "Just Works" pairing with no keyboard:
sudo cp bt-agent.service /etc/systemd/system/
sudo systemctl enable --now bt-agentA phone's AVRCP device otherwise pops a stray mouse cursor onto the screen. This udev rule tells libinput to ignore it:
sudo cp 99-pidash-ignore-avrcp-pointer.rules /etc/udev/rules.d/
sudo udevadm control --reloadDo this before enabling the service — the OAuth flows call input(), which a systemd service cannot answer. See Configuration below.
With no configuration at all the dashboard still runs: weather, markets, clock and the Bluetooth widget need no credentials.
The dashboard needs a graphical seat, so it cannot be launched from a plain SSH session. Run it from systemd, which also starts it on boot.
sudo cp ~/Pi_dashboard/pi-dashboard.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pi-dashboard
journalctl -u pi-dashboard -f # watch it startA healthy start logs Display ready: 1920x440 via SDL driver 'wayland'.
The unit hardcodes
/home/pi/Pi_dashboard. If your Pi username differs, editWorkingDirectoryandExecStartto match — otherwise the service restart-loops withcan't open file … No such file or directory.
After any code change:
sudo systemctl restart pi-dashboardWidget toggles (ENABLE_*) live at the top of main.py. Personal credentials do not — they live in files that are gitignored, so they never end up in the repository:
| File | Holds | Created by |
|---|---|---|
device_conf.json |
Bambu printer IP / serial / access code, Roborock email | you, from device_conf.example.json |
settings.json |
Weather ZIP code, screensaver timeout, screen-off on/off, motion-wake on/off, daily-restart on/off and hour | the on-screen Settings menu |
claude_creds.json |
Claude OAuth token | claude.interactive_auth() |
token.json |
Gmail OAuth token | gmail_auth.py |
main.py reads all of them at runtime. That is why you can safely publish your fork of this repo.
Easiest, and it keeps your location out of the repo — hold the screen for 5 seconds → Zip Code, type it on the numeric keypad, confirm. It's saved to settings.json and the weather re-resolves immediately, no restart.
Alternatively, edit main.py:
LOCATION_ZIP = '10001' # a postal code, resolved via zippopotam.us
LOCATION_ZIP_COUNTRY = 'us'The dashboard ships unpinned (LOCATION_ZIP = '', USE_IP_LOCATION = True), so out of the box the weather follows the Pi's public IP. That's convenient but a VPN or unusual ISP routing can place you in the wrong city — set a ZIP for an exact fix.
You do not need to enable LAN Mode on the printer.
- On the printer's screen go to Settings → Network and note the IP address, serial number and access code.
- Give the printer a static DHCP reservation on your router — otherwise its IP changes and the widget goes offline.
- Copy the example config and fill it in:
cp device_conf.example.json device_conf.json
nano device_conf.json{
"printer": {
"IP": "192.168.1.50",
"SERIAL": "your-printer-serial",
"ACCESS_CODE": "your-lan-access-code"
}
}- Ensure
ENABLE_BAMBU = Trueinmain.py, then restart the service.
device_conf.json is gitignored. Without it the printer widget simply shows PRINTER: OFFLINE rather than crashing.
Shows your Claude Code usage limits. Auth is a browser OAuth flow, so run it on your desktop, not the headless Pi.
On your desktop, in a copy of this repo:
python -c "import claude; claude.interactive_auth()"- It prints an authorization URL. Open it and log in with your Claude account.
- You'll be redirected to a dead
localhost:18924/callback?code=…page — that's expected. - Copy the full URL from the address bar and paste it back at the prompt.
- It writes
claude_creds.json.
Then copy it to the Pi:
scp claude_creds.json raspberrypi.local:~/Pi_dashboard/
ssh raspberrypi.local "sudo systemctl restart pi-dashboard"Set ENABLE_CLAUDE = True in main.py. The token self-renews from its refresh token, so this is a one-time step.
Shows your unread inbox count, read-only. There is no ENABLE_GMAIL flag — the widget activates as soon as a valid token.json is present.
In the Google Cloud Console (one-time):
- Create a project and enable the Gmail API.
- Configure the OAuth consent screen (type: External). Under Test users, add your own Gmail address — skip this and Google returns
access_denied. - Create an OAuth 2.0 Client ID of type Desktop app. Download the JSON, rename it to
credentials.json, and put it next togmail_auth.py.
On your desktop:
pip install google-auth-oauthlib google-api-python-client
python gmail_auth.pyA browser opens; grant read-only access. It writes token.json and prints your unread count to confirm.
Then copy it to the Pi:
scp token.json raspberrypi.local:~/Pi_dashboard/
ssh raspberrypi.local "sudo systemctl restart pi-dashboard"credentials.json stays on your desktop — the Pi only ever needs token.json, and it refreshes itself from then on.
Once the dashboard is running you never need a keyboard again. Hold the screen for 5 seconds → WiFi:
- Nearby networks are scanned and listed with signal strength and a lock icon for secured ones.
- Tap one, type the password on the on-screen keyboard (letters /
?123symbols / Shift), and tap Connect.
It drives nmcli, so NetworkManager saves the profile and reconnects on boot. Any stale profile for that SSID is cleared first, so a retry can't fail with key-mgmt: property is missing.
Hold the screen for 5 seconds to open it. Six sub-screens, each tile showing a live subtitle (current SSID, paired phone, ZIP, version):
| Screen | What it does |
|---|---|
| WiFi | Scan and join networks with an on-screen keyboard |
| Bluetooth | Pair a new phone (Pi_Dashboard), or forget a paired one |
| Zip Code | Numeric keypad for the weather ZIP; applied immediately |
| Account | The connected Claude and Google accounts; Edit opens a keyboard to set the Bambu printer's IP / access code / serial |
| Screensaver | The idle timeout stepper (1–60 min), a Screen Off on/off toggle (power the panel down 10 min into the screensaver), and the motion-wake on/off toggle with a live motion reading when an HC-SR501 is fitted |
| Firmware | App version, hostname, IP, current WiFi, Python version, uptime — plus the daily auto-restart on/off and hour stepper, and a two-tap Restart Now |
Close returns to the dashboard. Everything runs as root under the service, so there's no sudo prompt. The menu needs the app running under cage — it can't be driven over SSH.
- Create an API Application in your Strava API settings; note the Client ID and Client Secret.
- Set
ENABLE_STRAVA = Trueand runpython3 main.pyfrom a terminal. - It asks for the ID/secret and prints an authorization URL. Open it, click Authorize, and you'll land on a dead
localhostpage. - Paste the
code=…value back into the terminal. It savesactivity:read_alltokens tostrava_token.json.
- Put your Roborock account email in the
roborockblock ofdevice_conf.json. - Set
ENABLE_ROBOROCK = Trueand runpython3 main.pyfrom a terminal. - It requests a one-time password, emailed to you. Enter the 6-digit code; the session is saved locally.
Needs pip3 install --break-system-packages roborock aiomqtt.
The official Spotify API needs a local web server for token renewal, so the dashboard reads the current track from Last.fm instead.
- Connect your Spotify account to Last.fm.
- Create a Last.fm API account to get an API key.
- Fill in
LASTFM_CONFinmain.pywith the key and your Last.fm username, and setENABLE_SPOTIFY = True.
After setup you don't need to use Last.fm directly, and a paid account isn't required — keep using Spotify as normal.
| Flag | Purpose |
|---|---|
--preview [file.png] |
Render one frame to a PNG and exit. Needs no display — useful for checking layout over SSH. |
--windowed |
Run in a window instead of fullscreen (desktop testing). |
--theme {dark,light} |
Starting theme. Default dark. |
| Gesture | Action |
|---|---|
| Tap | Refetch all data (rate-limited to once per 15 s) |
| Press & hold ~1.2 s | Toggle light / dark theme |
| Press & hold 5 s | Open the Settings menu (a progress bar fills as you hold) |
| Tap during screensaver | Return to the dashboard (wakes only; does not also refresh) |
R / T / S / Esc,Q |
Refresh / theme / settings / quit (with a keyboard attached) |
- Asynchronous data fetching. Each service (weather, printer, markets, Claude, Gmail…) is polled by its own background thread at its own interval. A slow API or a dropped connection in one never blocks the others or freezes the UI.
- Change-driven rendering. The main loop polls touch at 30 Hz but only composes and blits a frame when something visible changes. Every data write bumps a revision counter; the renderer compares
(clock minute, revision, theme, dim state)against the last frame and skips the redraw if they match. An idle screen costs almost nothing.
Initial population is deliberately slow. On first launch widgets show placeholders or zeros and fill in over a few minutes. Initial requests are staggered on purpose — it avoids a CPU spike, spares the Pi's network stack, and respects the upstream APIs' rate limits.
This GeeekPi LCD exposes no backlight control (/sys/class/backlight is empty), ignores vcgencmd display_power under KMS, and does not support HDMI-CEC (it NACKs CEC commands). The one lever left is the connector itself: cage advertises wlr-output-management, so wlr-randr --output <name> --off disables the output rather than merely idling the signal, and --on brings it back with the pygame app still running underneath.
That distinction is the whole trick, and it was measured on this panel: disabling the connector takes it fully dark with the backlight off, no "No Signal" OSD. The OSD an earlier version of this README warned about appears when the signal merely stops while the connector stays enabled — not the same operation.
So idling happens in two stages:
- Drifting clock (
SCREENSAVER_SECONDS, default 10 min) — the time/day/date block moves around on black, which prevents burn-in from static content while the panel stays lit and instantly readable. - Screen off (
SCREEN_OFF_SECONDS, default 10 min later) — the connector is disabled, the backlight goes out, and rendering stops entirely.
Waking works from either stage because ScreenPower reads the kernel touch device (/dev/input/by-id/usb-ILITEK_*) directly in its own thread: while the output is disabled the compositor has no surface to route touches to, but the USB digitizer keeps emitting evdev events regardless of HDMI state. Motion on the PIR wakes it through the same idle timer.
Stage 2 is a toggle (Settings → Screensaver → Screen Off), so you can keep the panel permanently lit — or fall back to stage 1 alone if a different panel on this code reacts badly to a disabled connector. If wlr-randr, the output or the touch device can't be found, stage 2 disables itself and the button is replaced by an explanatory line — the screen can never get stuck dark. The app also forces the output back on at startup, so a crash or watchdog restart while dark still comes back lit.
If you're coming from the Waveshare 10.85" build:
display.pyreplaces the e-paper driver. Theepd10in85driver, its.soblobs and the SPI setup are no longer imported by anything. They live inReference/waveshare_epd/, off the import path, for rollback — deleteReference/once you're happy with the LCD.- Geometry changed from 1360x480 to 1920x440. The panel is 560 px wider but 40 px shorter, so the layout went from 3 columns to 4 and each column's vertical budget tightened.
- Colour replaces 1-bit. Icons load as alpha masks and are painted in whatever colour the theme specifies, so the same
icons/*.bmpfiles are reused unmodified. Album art stays full-colour RGB instead of being dithered. - The
signal.SIGALRMhardware watchdog is gone. It existed to recover from the e-paper's SPI busy-wait hangs; an HDMI blit cannot hang that way. - The 60-second refresh floor is gone. That was an e-ink hardware constraint, not a design choice.
- New in this fork: the Bluetooth now-playing widget, the on-screen settings menu (WiFi / Bluetooth / ZIP / account / firmware), the two-stage screensaver (moving clock, then panel off), the optional HC-SR501 motion wake sensor, and the light theme.
The 3D-printed case and assembly video from the original project were designed for the Waveshare panel and do not fit this one, so they aren't reproduced here — see the upstream repository if you're building the e-paper version.
