Bosch Bar puts your Bosch eBike Flow data (Tern GSD and any Bosch Smart System bike with a ConnectModule) in your Mac's menu bar:
- a native macOS menu bar app — live battery %, charge countdown, ride alerts, one-click sign-in, and auto-updates,
- a web dashboard — battery, per-mode efficiency, bike health, firmware, and a metric-colored ride map with GPX export,
- plus, for tinkerers: a local REST API, a zero-dependency CLI, and event webhooks (battery full/low, charging, ride completed, firmware…).
ℹ️ Same data as the Bosch app. "Bosch Bar" shows the exact battery, range and ride info you see in Bosch's eBike Flow app — just in your Mac's menu bar. (The app was renamed from "Bosch Flow" so it isn't confused with Bosch's own Flow app.)
⚠️ Unofficial. This talks to Bosch's private eBike Flow API using the mobile app's OAuth client (reverse-engineered). It is not endorsed by Bosch, may break if Bosch changes things, and is intended for personal use with your own account. See Limitations.
That's all most people need — download, open, sign in.
- Download the latest release »
and unzip
Bosch-Bar-*.zip. - Drag
Bosch Bar.appinto your Applications folder and open it. - It appears in your menu bar (🚲 + battery %). Click it → Sign in…, log in with your eBike Flow account in the browser that opens — done.
No Python, no terminal, no copying tokens. The app is Developer ID-signed and notarized (opens without warnings) and auto-updates itself. On first launch, click Allow when macOS asks about notifications so you get battery and ride alerts.
Everything below is for developers / power users — the CLI, the REST API, webhooks, and running the server yourself. If you just want the app, you're done. 🚲
- Requirements
- Setup
- Get a token (authenticate with Bosch)
- Run the API + dashboard
- Menu bar app (macOS)
- Events & webhooks
- Configuration
- API reference
- How it works
- Limitations & caveats
- Project layout
- Python 3.9+ (developed on 3.14)
- A Bosch eBike with a ConnectModule and an eBike Flow account
- macOS — only needed for the menu bar app + native notifications; the CLI, API, and dashboard run anywhere
- For the menu bar app: Xcode command-line tools (
swiftc)
git clone <this repo> bosch-flow && cd bosch-flow
python3 -m venv .venv
./.venv/bin/pip install -r requirements.txtThat's it for the server. The CLI (bosch_ebike_poc.py) needs no dependencies
at all (pure standard library).
This is the only fiddly part. Bosch's OAuth client redirects to an iOS app deep
link (onebikeapp-ios://…) that a browser can't follow, so you have to grab the
authorization code out of the browser's developer tools by hand. You do this
once; after that the token auto-refreshes.
python3 bosch_ebike_poc.py loginIt prints a long https://p9.authz.bosch.com/… URL. Then:
- Open that URL in a desktop browser (Chrome or Firefox).
- Open DevTools → Network tab before logging in, and enable "Preserve log" (Chrome) / "Persist Logs" (Firefox).
- Log in with your eBike Flow app credentials (same as the phone app).
- After login the page tries to redirect to
onebikeapp-ios://com.bosch.ebike.onebikeapp/oauth2redirect?code=…and fails — that's expected (browsers can't open an iOS app link). - In the Network tab, find that failed
onebikeapp-ios://…request. Copy the value of itscodequery parameter — a very long (500–1000+ char) string. (You can also copy the wholeonebikeapp-ios://…URL; the tool will extract the code.) - Paste it back into the CLI prompt and press Enter.
The tokens are saved to bosch_tokens.json. Verify:
python3 bosch_ebike_poc.py bikes # list your bike(s)
python3 bosch_ebike_poc.py fetch # battery, range, odometer…The API server auto-imports bosch_tokens.json on first run, so once the CLI
login works, the server and dashboard are authenticated too.
If you'd rather not use the CLI:
# 1) start the server (see section 3), then:
curl -s http://127.0.0.1:8099/api/auth/login
# -> {"auth_url": "...", "state": "..."}Do steps 1–5 above with the returned auth_url, then exchange the code:
curl -s -XPOST http://127.0.0.1:8099/api/auth/callback \
-H 'content-type: application/json' \
-d '{"code":"<PASTED_CODE_OR_URL>", "state":"<STATE_FROM_LOGIN>"}'
# -> {"user": "<rider-id>", "bikes": [...]}Tokens are stored in bosch_users.json (keyed by your Bosch rider id) and
auto-refreshed (the access token lasts ~2h; a refresh token is used silently).
| File | Written by | Contains |
|---|---|---|
bosch_tokens.json |
CLI login |
single-user access + refresh token |
bosch_users.json |
API /auth/callback |
multi-user token store |
Delete these to log out. If a refresh ever fails (e.g. Bosch invalidated it),
just run login again.
On macOS you can skip this — the menu bar app embeds and runs this backend for you. Use the steps below for development, non-Mac hosts, or to run the server headless.
./.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port 8099 --reload
# or, honoring BOSCH_FLOW_DATA_DIR / HOST / PORT:
./.venv/bin/python serve.pyOpen http://127.0.0.1:8099/ — the dashboard shows battery, range per assist mode, lifetime mode efficiency, firmware inventory, a map of your rides, and GPX downloads. Hit ⟳ Refresh to force a fresh pull past the cache.
Click any ride to draw its GPS track colored by metric — toggle between
Speed / Power / Cadence (top-right of the map) to see where you were fast vs.
where you actually put power down. Deep-link straight to a ride with
?ride=<activity-id>&metric=power_w.
Note: Bosch records the assist mode only as a whole-ride total (distance per mode), not per GPS point — so the track can be colored by speed/power/cadence, but not by which assist mode you were in at each point.
The server also starts a background poller (every 5 min) that powers events, webhooks, and notifications.
A native menu bar app that shows live battery % and posts native desktop notifications on events (this is the reliable way to get banners — a proper app gets its own notification permission).
It's self-contained — no separate server, no Python needed. The app embeds
the whole backend (section 3) as a frozen binary, launches it on startup, and
shuts it down on quit. You can still curl http://127.0.0.1:8099 or open the
dashboard while the app is running; it's the same REST API, just supervised for
you. Its token/state files live in ~/Library/Application Support/Bosch Bar/
(with a backend.log there if you need to debug).
Prebuilt: grab the latest signed build from
GitHub Releases —
unzip and drag to /Applications (no Xcode needed). It's Developer ID-signed and
notarized by Apple, so it opens without warnings, and auto-updates itself via
Sparkle from then on. If you build/sign it yourself and macOS flags it, right-click →
Open once.
Maintainers: menubar/notarize.sh does the full Developer-ID sign → notarize →
staple → package flow (needs a notarytool keychain profile and an in-effect Apple
Developer agreement). It signs the embedded backend inside-out with the entitlements
CPython needs under the hardened runtime.
Or build it yourself:
cd menubar
../.venv/bin/pip install -r ../requirements-build.txt # once: PyInstaller
./build.sh # compiles Swift + freezes the backend into the .app
open "Bosch Bar.app"On first launch macOS asks "Bosch Bar" would like to send notifications — click Allow (this is what makes banners work).
- Menu bar shows
🚲 100%(⚡ when charging). - Click it for a popover: battery, ranges, odometer, latest ride, Dashboard and Refresh buttons.
- Log in / Update token… — signs you into Bosch right from the app: it opens
the Bosch login in your browser and captures the
onebikeapp-ios://redirect automatically (the app registers that URL scheme). If the hand-off doesn't fire, Paste code… takes the code (or the whole redirect URL) instead. No CLI, DevTools, orcurlneeded for setup. - Launch at login — a checkbox that enrolls the app as a macOS login item
(via
SMAppService); untick to remove it. - Check for Updates… — the app auto-updates itself via Sparkle: it checks for a new version in the background and can install it with one click. No App Store needed.
- New events (battery full, ride logged, …) fire native banners.
Building the app requires PyInstaller (
requirements-build.txt) in the venv andswiftc(Xcode command-line tools). The runtime still needs nothing but the frozen bundle.
Auto-update (maintainers). Updates ship as notarized zips on GitHub Releases;
the app's SUFeedURL reads releases/latest/download/appcast.xml. Cut a release with:
cd menubar
./make-release.sh <version> <build#> "release notes"
# e.g. ./make-release.sh 1.1 2 "Embedded backend, in-app login, unit sync."That builds + notarizes at the given version (notarize.sh), EdDSA-signs the zip for
Sparkle (sparkle_sign.swift, using ~/.appstoreconnect/private_keys/sparkle_ed25519.key),
generates appcast.xml, and publishes both to GitHub Releases via gh. The build
number must increase every release — Sparkle compares it to detect updates. The
Sparkle framework is vendored at menubar/vendor/Sparkle.framework; notarize.sh
signs its nested helpers inside-out.
Bosch's API is poll-only (no push). The poller diffs each fresh snapshot and emits edge-triggered events:
battery.full · battery.low · battery.charging_started ·
battery.charging_stopped · charger.connected · charger.disconnected ·
ride.completed · firmware.changed
Manage webhooks from the dashboard's Webhooks button (add/delete/test, pick events, see a live event feed), or via the API — register one (fired with an HMAC signature):
curl -s -XPOST http://127.0.0.1:8099/api/webhooks \
-H 'content-type: application/json' \
-d '{"url":"https://example.com/hook","events":["battery.full","ride.completed"]}'
# -> returns a "secret" ONCE; use it to verify signaturesEach delivery carries:
X-Bosch-Flow-Event: battery.full
X-Bosch-Flow-Signature: sha256=<hmac_sha256(secret, raw_body)>
Verify by recomputing the HMAC over the raw request body. See
API reference for GET /api/events, POST /api/poll, etc.
Edit app/config.py:
| Setting | Default | Meaning |
|---|---|---|
POLL_INTERVAL |
300 |
seconds between poll cycles |
BATTERY_LOW_PCT |
20 |
threshold for battery.low |
TTL_LIVE / TTL_PROFILE / TTL_RIDES |
300 / 900 / 600 | cache TTLs (s) |
NOTIFY_ENABLED |
False |
fire terminal-notifier from the server (the menu bar app handles notifications instead) |
WEBHOOK_RETRIES |
3 |
delivery attempts w/ backoff |
The macOS notification sender identity is in NOTIFY_SENDER (only used when
NOTIFY_ENABLED = True).
Environment variables (read by serve.py, and set automatically by the menu
bar app):
| Var | Default | Meaning |
|---|---|---|
BOSCH_FLOW_DATA_DIR |
repo root | where tokens/state/event log are written (the app points this at ~/Library/Application Support/Bosch Flow) |
BOSCH_FLOW_HOST |
127.0.0.1 |
bind host |
BOSCH_FLOW_PORT |
8099 |
bind port |
| Endpoint | Returns |
|---|---|
GET /api/bikes |
bikes on the account |
GET /api/bikes/{id}/battery |
live SoC, charging, range per mode, odometer |
GET /api/bikes/{id}/profile |
model, ABS/alarm, firmware inventory, health |
GET /api/bikes/{id}/stats |
per-mode lifetime distance + Wh/km |
GET /api/bikes/{id}/rides |
ride list (?gps=1 adds has_gps/gps_points) |
GET /api/bikes/{id}/rides/{aid} |
ride summary + GPS time series |
GET /api/bikes/{id}/rides/{aid}.gpx |
ride as GPX |
GET /api/bikes/{id}/tracks.geojson |
all GPS tracks as one layer |
GET /api/bikes/{id}/location |
last-known position (newest GPS ride) |
GET /api/events?limit=50 |
recent event log |
POST /api/poll |
run one poll cycle now (?dispatch=0 = detect only) |
GET/POST /api/webhooks, DELETE /api/webhooks/{id}, POST /api/webhooks/{id}/test |
manage webhooks |
GET /api/auth/login, POST /api/auth/callback |
authentication |
Add ?fresh=1 to any data endpoint to bypass the cache. Select a user (multi-user
setups) with X-Bike-User: <rider-id> or ?user=.
Bosch splits data across several hosts under *.prod.connected-biking.cloud,
behind an OAuth2 + PKCE Keycloak (p9.authz.bosch.com, realm obc, client
one-bike-app). This project is a facade over that:
app/auth.py— OAuth2 + PKCE (build URL, exchange code, refresh).app/bosch.py— async client that hides tokens, caches per Bosch's cadence, and normalizes the raw JSON. Data comes fromobc-rider-profile(/v1/bike-profile,/v1/state-of-charge) andobc-rider-activity(/v1/activity,/v1/activity/{id}/detail— the source of GPS tracks).app/poller.py— background loop → snapshot diff → events → webhooks.
Live data (state-of-charge) is only fresh when the ConnectModule is charging,
powered on, or alarmed; otherwise you get last-known values.
- Unofficial / ToS. Uses the eBike Flow app's OAuth client. Fine for personal use with your own account; not a basis for a public multi-user product — that needs official Bosch Connected Biking Platform partner API access (your own client + redirect URIs → a real one-click web login).
- Login uses the app's deep-link redirect. There's no hosted browser-redirect
login, so the CLI/API path relies on copying the
codefrom the redirect. The menu bar app closes this loop: it registers theonebikeapp-ios://scheme and captures the redirect automatically (with a paste fallback). - Location. GPS tracks exist only for rides recorded with the phone. The app's live "where's my bike" pin (anti-theft) is served by a separate host whose exact route isn't mapped here.
- Notifications are local to whichever machine runs the menu bar app; use webhooks to reach elsewhere.
- Sync lag. Rides appear only after the bike syncs to Bosch's cloud (open the Flow app near the powered-on bike); the odometer updates independently and sooner.
bosch_ebike_poc.py zero-dependency CLI (login / bikes / fetch / raw)
serve.py backend entry point (honors BOSCH_FLOW_DATA_DIR/HOST/PORT)
requirements.txt server deps (starlette, uvicorn, httpx)
requirements-build.txt build-only deps (PyInstaller, to freeze the backend)
app/
config.py hosts, OAuth, cache TTLs, poller + notify, data dir
auth.py OAuth2 + PKCE helpers
store.py multi-user, file-backed token store
bosch.py async facade: raw endpoints + normalization + cache
tracks.py GPX / GeoJSON rendering
events.py pure event-detection rules (snapshot diff)
webhooks.py webhook subscriptions + signed delivery
poller.py background poll loop + event log
notify.py (optional) server-side macOS notifications
main.py Starlette routes + serves the dashboard
web/index.html the dashboard (Leaflet map + cards)
menubar/
BoschBar.swift menu bar app: supervises backend + login + notifications
makeicon.swift app-icon generator
build.sh compile Swift + freeze backend + bundle + sign the .app
notarize.sh Developer-ID sign (inside-out) + notarize + staple
Token / state files (git-ignored, created at runtime): bosch_tokens.json,
bosch_users.json, bosch_webhooks.json, bosch_poller_state.json,
bosch_events.jsonl.

